From fb57943b240f45e2350d97c122ecf41d11d13e21 Mon Sep 17 00:00:00 2001 From: Gergely Nemeth Date: Fri, 12 Aug 2016 12:09:08 +0200 Subject: [PATCH] feat(metrics): add custom metrics --- README.md | 22 ++++++++++ lib/agent/api/index.js | 14 ++++++ lib/agent/api/index.spec.js | 1 + lib/agent/index.js | 6 +++ lib/agent/metrics/custom/index.js | 61 ++++++++++++++++++++++++++ lib/agent/metrics/custom/index.spec.js | 41 +++++++++++++++++ lib/agent/metrics/index.js | 1 + lib/config.js | 1 + lib/trace.js | 8 ++++ lib/trace.spec.js | 25 +++++++++++ test/e2e/initialization.spec.js | 1 + 11 files changed, 181 insertions(+) create mode 100644 lib/agent/metrics/custom/index.js create mode 100644 lib/agent/metrics/custom/index.spec.js diff --git a/README.md b/README.md index 4a138a5..26cd0fa 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,34 @@ current logging systems. var transactionId = trace.getTransactionId(); ``` +### trace.recordMetric(name, value) + +This method can be used to record custom metrics values. + +```javascript +trace.recordMetric('user/imageUpload', 6) +``` + +The name must have the following format: `/` +The value must be a number. + +### trace.incrementMetric(name, [amount]) + +This method can be used to record increment-only type of metrics. + +```javascript +trace.incrementMetric('user/signup') +``` + +The name must have the following format: `/` + ## Compatibility with Node versions * node v0.10@latest * node v0.12@latest * node v4@latest * node v5@latest +* node v6@latest ## Migrating from 1.x to 2.x diff --git a/lib/agent/api/index.js b/lib/agent/api/index.js index 9606a9b..7ad1730 100644 --- a/lib/agent/api/index.js +++ b/lib/agent/api/index.js @@ -20,6 +20,7 @@ function CollectorApi (options) { this.COLLECTOR_API_PROFILER_MEMORY_HEAPDUMP = url.resolve(options.collectorApiUrl, options.collectorApiProfilerMemoryHeapdumpEndpoint) this.COLLECTOR_API_PROFILER_CPU_PROFILE = url.resolve(options.collectorApiUrl, options.collectorApiProfilerCpuProfileEndpoint) this.COLLECTOR_API_CONTROL = url.resolve(options.collectorApiUrl, options.collectorApiControlEndpoint) + this.COLLECTOR_API_CUSTOM_METRICS = url.resolve(options.collectorApiUrl, options.collectorApiCustomMetrics) this.collectorLanguage = options.collectorLanguage this.apiKey = options.apiKey @@ -193,6 +194,19 @@ CollectorApi.prototype.getUpdates = function (data, callback) { }) } +CollectorApi.prototype.sendCustomMetrics = function (data) { + if (!isNumber(this.serviceKey)) { + debug('Service id not present, cannot send metrics') + return + } + + var url = util.format(this.COLLECTOR_API_CUSTOM_METRICS, this.serviceKey) + this._send(url, this._withInstanceInfo({ + timestamp: (new Date()).toISOString(), + data: data + })) +} + CollectorApi.prototype.sendSamples = function (samples, sync) { var url = this.COLLECTOR_API_SAMPLE var metadata = { diff --git a/lib/agent/api/index.spec.js b/lib/agent/api/index.spec.js index e00660f..6c5d09b 100644 --- a/lib/agent/api/index.spec.js +++ b/lib/agent/api/index.spec.js @@ -25,6 +25,7 @@ describe('The Trace CollectorApi module', function () { collectorApiProfilerMemoryHeapdumpEndpoint: '/service/%s/memory-heapdump', collectorApiProfilerCpuProfileEndpoint: '/service/%s/cpu-profile', collectorApiControlEndpoint: '/service/%s/control', + collectorApiCustomMetrics: '/service/%s/custom-metrics', system: { hostname: 'test.org', processVersion: '4.3.1', diff --git a/lib/agent/index.js b/lib/agent/index.js index ca723ad..d5ff2bb 100644 --- a/lib/agent/index.js +++ b/lib/agent/index.js @@ -74,6 +74,11 @@ function Agent (options) { controlBus: controlBus }) + this.customMetrics = Metrics.customMetrics.create({ + collectorApi: this.collectorApi, + config: this.config + }) + // TODO: The Tracer agent, to be extracted this.name = 'Tracer' @@ -102,6 +107,7 @@ function Agent (options) { this.rpmMetrics, this.externalEdgeMetrics, this.incomingEdgeMetrics, + this.customMetrics, this.memoryProfiler, this.cpuProfiler, this.control diff --git a/lib/agent/metrics/custom/index.js b/lib/agent/metrics/custom/index.js new file mode 100644 index 0000000..fc483c3 --- /dev/null +++ b/lib/agent/metrics/custom/index.js @@ -0,0 +1,61 @@ +var debug = require('debug')('risingstack/trace') + +var Timer = require('../../timer') + +function CustomMetrics (options) { + this.name = 'Metrics/Custom' + var _this = this + this.collectorApi = options.collectorApi + this.config = options.config + this.collectInterval = this.config.collectInterval + + this.incrementMetrics = {} + this.recordMetrics = {} + + this.timer = new Timer(function () { + _this.sendMetrics() + }, this.collectInterval) +} + +CustomMetrics.prototype.increment = function (name, amount) { + if (!name) { + throw new Error('Name is needed for CustomMetrics.increment') + } + amount = amount || 1 + if (this.incrementMetrics[name]) { + this.incrementMetrics[name] += amount + } else { + this.incrementMetrics[name] = amount + } +} + +CustomMetrics.prototype.record = function (name, value) { + if (!name) { + throw new Error('Name is needed for CustomMetrics.record') + } + if (typeof value === 'undefined') { + throw new Error('Name is needed for CustomMetrics.record') + } + + if (this.recordMetrics[name]) { + this.recordMetrics[name].push(value) + } else { + this.recordMetrics[name] = [value] + } +} + +CustomMetrics.prototype.sendMetrics = function () { + this.collectorApi.sendCustomMetrics({ + incrementMetrics: this.incrementMetrics, + recordMetrics: this.recordMetrics + }) + + this.incrementMetrics = {} + this.recordMetrics = {} +} + +function create (options) { + return new CustomMetrics(options) +} + +module.exports.create = create diff --git a/lib/agent/metrics/custom/index.spec.js b/lib/agent/metrics/custom/index.spec.js new file mode 100644 index 0000000..46d3ab0 --- /dev/null +++ b/lib/agent/metrics/custom/index.spec.js @@ -0,0 +1,41 @@ +var expect = require('chai').expect + +var CustomMetrics = require('./') + +describe('The CustomMetrics module', function () { + it('sends metrics', function () { + var ISOString = 'date-string' + var collectorApi = { + sendCustomMetrics: this.sandbox.spy() + } + + this.sandbox.stub(Date.prototype, 'toISOString', function () { + return ISOString + }) + + var customMetrics = CustomMetrics.create({ + collectorApi: collectorApi, + config: { + collectInterval: 1 + } + }) + + customMetrics.increment('/TestCategory/TestName') + customMetrics.increment('/TestCategory/TestName') + customMetrics.increment('/TestCategory/TestName', 3) + + customMetrics.record('/TestCategory/TestRecord', 10) + customMetrics.record('/TestCategory/TestRecord', 2) + + customMetrics.sendMetrics() + + expect(collectorApi.sendCustomMetrics).to.be.calledWith({ + incrementMetrics: { + '/TestCategory/TestName': 5 + }, + recordMetrics: { + '/TestCategory/TestRecord': [10, 2] + } + }) + }) +}) diff --git a/lib/agent/metrics/index.js b/lib/agent/metrics/index.js index e7615a8..891c05e 100644 --- a/lib/agent/metrics/index.js +++ b/lib/agent/metrics/index.js @@ -2,3 +2,4 @@ module.exports.apm = require('./apm') module.exports.rpm = require('./rpm') module.exports.incomingEdge = require('./incomingEdge') module.exports.externalEdge = require('./externalEdge') +module.exports.customMetrics = require('./custom') diff --git a/lib/config.js b/lib/config.js index 4b5b2d7..9bb994f 100644 --- a/lib/config.js +++ b/lib/config.js @@ -17,6 +17,7 @@ config.collectorApiHealthcheckEndpoint = '/service/%s/healthcheck' config.collectorApiProfilerMemoryHeapdumpEndpoint = '/service/%s/memory-heapdump' config.collectorApiProfilerCpuProfileEndpoint = '/service/%s/cpu-profile' config.collectorApiControlEndpoint = '/service/%s/control' +config.collectorApiCustomMetrics = '/service/%s/custom-metrics' config.configPath = 'trace.config' diff --git a/lib/trace.js b/lib/trace.js index eb84c93..44c83bb 100644 --- a/lib/trace.js +++ b/lib/trace.js @@ -52,6 +52,14 @@ Trace.prototype.sendMemorySnapshot = function () { this._agent.memoryProfiler.sendSnapshot() } +Trace.prototype.incrementMetric = function (name, amount) { + this._agent.customMetrics.increment(name, amount) +} + +Trace.prototype.recordMetric = function (name, value) { + this._agent.customMetrics.record(name, value) +} + module.exports.Trace = Trace module.exports.noop = traceNoop diff --git a/lib/trace.spec.js b/lib/trace.spec.js index 6459ea2..90dfaa0 100644 --- a/lib/trace.spec.js +++ b/lib/trace.spec.js @@ -56,6 +56,10 @@ describe('Trace', function () { report: sinon.spy(), reportError: sinon.spy(), getRequestId: sinon.spy(), + customMetrics: { + increment: sinon.spy(), + record: sinon.spy() + }, memoryProfiler: { } } @@ -64,6 +68,8 @@ describe('Trace', function () { fakeAgent.report.reset() fakeAgent.reportError.reset() fakeAgent.getRequestId.reset() + fakeAgent.customMetrics.increment.reset() + fakeAgent.customMetrics.record.reset() }) it('is a constructor', function () { @@ -147,6 +153,25 @@ describe('Trace', function () { expect(fakeAgent.getRequestId).to.have.been.called }) }) + + describe('customMetrics', function () { + it('is a function', function () { + this.sandbox.stub(Agent, 'create').returns(fakeAgent) + this.sandbox.stub(Instrumentation, 'create').returns({}) + var instance = new trace.Trace(fakeConfig) + expect(instance.incrementMetric).to.be.a('function') + expect(instance.recordMetric).to.be.a('function') + }) + it('calls agent.customMetrics', function () { + this.sandbox.stub(Agent, 'create').returns(fakeAgent) + this.sandbox.stub(Instrumentation, 'create').returns({}) + var instance = new trace.Trace(fakeConfig) + instance.incrementMetric() + instance.recordMetric() + expect(fakeAgent.customMetrics.record).to.have.been.called + expect(fakeAgent.customMetrics.increment).to.have.been.called + }) + }) }) }) diff --git a/test/e2e/initialization.spec.js b/test/e2e/initialization.spec.js index d5f9824..6ae6f1f 100644 --- a/test/e2e/initialization.spec.js +++ b/test/e2e/initialization.spec.js @@ -70,6 +70,7 @@ test('should get service key', t.equal(requestBody.version, '2', 'schema version ok') t.equal(requestBody.collector.version, pkg.version, 'collector version ok') t.end() + process.exit(0) } }) require('@risingstack/trace')