From 2c2a4e2190b8dce4f7a28c49167f7edb620c6810 Mon Sep 17 00:00:00 2001 From: Gergely Nemeth Date: Mon, 21 Mar 2016 15:57:05 +0100 Subject: [PATCH] feat(topology): add generic topology support --- e2e/reportApmMetrics.spec.js | 8 ++ e2e/reportHttpRequest.spec.js | 5 + e2e/utils/serviceMocks.js | 12 ++ lib/agent/api/index.js | 24 +++- lib/agent/api/index.spec.js | 3 +- lib/agent/index.js | 35 ++++- lib/agent/metrics/edge/index.js | 119 ++++++++++++++++ lib/agent/metrics/edge/index.spec.js | 127 ++++++++++++++++++ lib/agent/metrics/index.js | 1 + lib/config.js | 1 + lib/consts.js | 7 +- lib/instrumentations/core/http/request.js | 21 ++- .../core/http/request.spec.js | 14 +- lib/instrumentations/index.js | 1 + lib/instrumentations/mongodb.js | 63 +++++++++ lib/instrumentations/mongodb.spec.js | 114 ++++++++++++++++ lib/instrumentations/mysql.js | 18 ++- lib/instrumentations/mysql.spec.js | 83 ++++++++++++ 18 files changed, 634 insertions(+), 22 deletions(-) create mode 100644 lib/agent/metrics/edge/index.js create mode 100644 lib/agent/metrics/edge/index.spec.js create mode 100644 lib/instrumentations/mongodb.js create mode 100644 lib/instrumentations/mongodb.spec.js create mode 100644 lib/instrumentations/mysql.spec.js diff --git a/e2e/reportApmMetrics.spec.js b/e2e/reportApmMetrics.spec.js index 46c96e5..e3f0bc3 100644 --- a/e2e/reportApmMetrics.spec.js +++ b/e2e/reportApmMetrics.spec.js @@ -23,6 +23,14 @@ test('should report apm metrics', function (t) { function (uri, requestBody) { t.pass('collector sent rpm metrics') }) + serviceMocks.mockEdgeMetricsRequest( + TRACE_COLLECTOR_API_URL, + TRACE_API_KEY_TEST, + TRACE_SERVICE_KEY_TEST, + 1, + function (uri, requestBody) { + t.pass('collector sent edge metrics') + }) serviceMocks.mockApmMetricsRequest( TRACE_COLLECTOR_API_URL, TRACE_API_KEY_TEST, diff --git a/e2e/reportHttpRequest.spec.js b/e2e/reportHttpRequest.spec.js index 85be96e..2d1f431 100644 --- a/e2e/reportHttpRequest.spec.js +++ b/e2e/reportHttpRequest.spec.js @@ -27,6 +27,11 @@ test('should report http requests', function (t) { TRACE_API_KEY_TEST, 42, Number.MAX_SAFE_INTEGER) // set the times parameter high so the http mock catches all + serviceMocks.mockEdgeMetricsRequest( + TRACE_COLLECTOR_API_URL, + TRACE_API_KEY_TEST, + 42, + Number.MAX_SAFE_INTEGER) serviceMocks.mockHttpTransactionRequest( TRACE_COLLECTOR_API_URL, TRACE_API_KEY_TEST, diff --git a/e2e/utils/serviceMocks.js b/e2e/utils/serviceMocks.js index 56d4763..eabf1ed 100644 --- a/e2e/utils/serviceMocks.js +++ b/e2e/utils/serviceMocks.js @@ -42,6 +42,17 @@ function mockRpmMetricsRequest (url, apiKey, serviceKey, maxTimes, callback) { .reply(callback || 200) } +function mockEdgeMetricsRequest (url, apiKey, serviceKey, maxTimes, callback) { + return nock(url, { + reqheaders: { + 'Authorization': 'Bearer ' + apiKey + } + }) + .post('/service/42/edge-metrics') + .times(maxTimes) + .reply(callback || 200) +} + function mockHttpTransactionRequest (url, apiKey, callback) { return nock(url, { reqheaders: { @@ -56,5 +67,6 @@ module.exports = { mockServiceKeyRequest: mockServiceKeyRequest, mockApmMetricsRequest: mockApmMetricsRequest, mockRpmMetricsRequest: mockRpmMetricsRequest, + mockEdgeMetricsRequest: mockEdgeMetricsRequest, mockHttpTransactionRequest: mockHttpTransactionRequest } diff --git a/lib/agent/api/index.js b/lib/agent/api/index.js index 1a79684..72f2fd2 100644 --- a/lib/agent/api/index.js +++ b/lib/agent/api/index.js @@ -15,6 +15,7 @@ function CollectorApi (options) { this.COLLECTOR_API_SERVICE = url.resolve(options.collectorApiUrl, options.collectorApiServiceEndpoint) this.COLLECTOR_API_METRICS = url.resolve(options.collectorApiUrl, options.collectorApiApmMetricsEndpoint) this.COLLECTOR_API_RPM_METRICS = url.resolve(options.collectorApiUrl, options.collectorApiRpmMetricsEndpoint) + this.COLLECTOR_API_EDGE_METRICS = url.resolve(options.collectorApiUrl, options.collectorApiEdgeMetricsEndpoint) this.apiKey = options.apiKey this.processId = options.processId @@ -38,6 +39,13 @@ CollectorApi.prototype.sendSync = function (data) { }) } +CollectorApi.prototype._sendWithPid = function (destinationUrl, data) { + this._send(destinationUrl, assign({ + hostname: this.hostname, + pid: this.processId + }, data)) +} + CollectorApi.prototype._send = function (destinationUrl, data) { var opts = url.parse(destinationUrl) var payload = JSON.stringify(data) @@ -77,7 +85,7 @@ CollectorApi.prototype.sendRpmMetrics = function (data) { } var url = util.format(this.COLLECTOR_API_RPM_METRICS, this.serviceKey) - this._send(url, assign({ hostname: this.hostname, pid: this.processId }, data)) + this._sendWithPid(url, data) } CollectorApi.prototype.sendApmMetrics = function (data) { @@ -87,12 +95,22 @@ CollectorApi.prototype.sendApmMetrics = function (data) { } var url = util.format(this.COLLECTOR_API_METRICS, this.serviceKey) - this._send(url, assign({ hostname: this.hostname, pid: this.processId }, data)) + this._sendWithPid(url, data) +} + +CollectorApi.prototype.sendEdgeMetrics = function (data) { + if (isNaN(this.serviceKey)) { + debug('Service id not present, cannot send metrics') + return + } + + var url = util.format(this.COLLECTOR_API_EDGE_METRICS, this.serviceKey) + this._sendWithPid(url, data) } CollectorApi.prototype.sendSamples = function (data) { var url = this.COLLECTOR_API_SAMPLE - this._send(url, assign({ hostname: this.hostname, pid: this.processId }, data)) + this._sendWithPid(url, data) } CollectorApi.prototype._getRetryInterval = function () { diff --git a/lib/agent/api/index.spec.js b/lib/agent/api/index.spec.js index 42a2c5c..81ecc96 100644 --- a/lib/agent/api/index.spec.js +++ b/lib/agent/api/index.spec.js @@ -17,7 +17,8 @@ describe('The Trace CollectorApi module', function () { collectorApiApmMetricsEndpoint: '/service/%s/apm-metrics', collectorApiRpmMetricsEndpoint: '/service/%s/rpm-metrics', hostname: 'test.org', - processId: '7777' + processId: '7777', + collectorApiEdgeMetricsEndpoint: '/service/%s/edge-metrics' } it('can be instantiated w/ serviceName and apiKey', function () { diff --git a/lib/agent/index.js b/lib/agent/index.js index 3147c88..f0a923a 100644 --- a/lib/agent/index.js +++ b/lib/agent/index.js @@ -40,6 +40,11 @@ function Agent (options) { config: this.config }) + this.edgeMetrics = Metrics.edge.create({ + collectorApi: this.collectorApi, + config: this.config + }) + this.collectorApi.getService(function (err, serviceKey) { if (err) { return debug(err.message) @@ -83,7 +88,7 @@ Agent.prototype.serverReceive = function (data) { span.parent = parentId span.events.push({ id: spanId, - time: data.time || microtime.now(), + time: data.time || this.getMicrotime(), type: Agent.SERVER_RECV }) } @@ -100,7 +105,7 @@ Agent.prototype.serverSend = function (data) { span.statusCode = data.statusCode span.events.push({ id: spanId, - time: data.time || microtime.now(), + time: data.time || this.getMicrotime(), type: Agent.SERVER_SEND }) @@ -123,7 +128,7 @@ Agent.prototype.clientSend = function (data) { return } - data.time = data.time || microtime.now() + data.time = data.time || this.getMicrotime() if (data.err) { span.isForceSampled = true @@ -156,11 +161,23 @@ Agent.prototype.clientSend = function (data) { Agent.prototype.clientReceive = function (data) { var span = this.findSpan(data.id) + this.edgeMetrics.report({ + targetHost: data.host, + targetServiceKey: data.targetServiceKey, + protocol: data.protocol, + networkDelay: { + incoming: data.networkDelayIncoming, + outgoing: data.networkDelayOutgoing + }, + status: data.status, + responseTime: data.responseTime + }) + if (!span) { return } - data.time = data.time || microtime.now() + data.time = data.time || this.getMicrotime() span.events.push({ host: data.host, @@ -183,7 +200,7 @@ Agent.prototype.onCrash = function (data) { span.isForceSampled = true span.events.push({ - time: microtime.now(), + time: this.getMicrotime(), id: spanId, type: 'err', data: { @@ -214,7 +231,7 @@ Agent.prototype.report = function (name, userData) { var dataToSend = { id: spanId, - time: microtime.now(), + time: this.getMicrotime(), type: 'us', data: { name: name, @@ -249,7 +266,7 @@ Agent.prototype.reportError = function (errorName, error) { var dataToSend = { id: spanId, - time: microtime.now(), + time: this.getMicrotime(), type: 'us', data: { name: errorName, @@ -354,6 +371,10 @@ Agent.prototype.generateId = function () { return uuid.v4() } +Agent.prototype.getMicrotime = function () { + return microtime.now() +} + Agent.prototype._send = function (options) { debug('sending logs to the trace service') if (this.spans.length > 0) { diff --git a/lib/agent/metrics/edge/index.js b/lib/agent/metrics/edge/index.js new file mode 100644 index 0000000..b36119e --- /dev/null +++ b/lib/agent/metrics/edge/index.js @@ -0,0 +1,119 @@ +var consts = require('../../../consts') + +function EdgeMetrics (options) { + var _this = this + this.collectorApi = options.collectorApi + this.config = options.config + this.collectInterval = this.config.collectInterval + + // metrics + this.metrics = {} + + this.interval = setInterval(function () { + _this.sendMetrics() + }, this.collectInterval) +} + +EdgeMetrics.prototype.initHost = function (data) { + if (!this.metrics[data.protocol][data.targetHost]) { + this.metrics[data.protocol][data.targetHost] = { + targetServiceKey: data.targetServiceKey, + responseTime: [], + networkDelayIncoming: [], + networkDelayOutgoing: [], + status: { + ok: 0, + notOk: 0 + } + } + } + return this.metrics[data.protocol][data.targetHost] +} + +EdgeMetrics.prototype.initProtocol = function (data) { + if (!this.metrics[data.protocol]) { + this.metrics[data.protocol] = {} + } + return this.metrics[data.protocol] +} + +EdgeMetrics.prototype.report = function (data) { + this.initProtocol(data) + var edge = this.initHost(data) + + if (data.networkDelay.incoming) { + edge.networkDelayIncoming.push(data.networkDelay.incoming) + } + + if (data.networkDelay.outgoing) { + edge.networkDelayOutgoing.push(data.networkDelay.outgoing) + } + + edge.responseTime.push(data.responseTime) + + if (data.status === consts.EDGE_STATUS.OK) { + edge.status.ok += 1 + } else if (data.status === consts.EDGE_STATUS.NOT_OK) { + edge.status.notOk += 1 + } +} + +EdgeMetrics.prototype.calculateTimes = function (items) { + var sorted = items.sort(function (a, b) { + return a - b + }) + + var medianElementIndex = Math.round(sorted.length / 2) - 1 + var ninetyFiveElementIndex = Math.round(sorted.length * 0.95) - 1 + + return { + median: sorted[medianElementIndex], + ninetyFive: sorted[ninetyFiveElementIndex] + } +} + +EdgeMetrics.prototype.sendMetrics = function () { + var _this = this + + var metrics = Object.keys(this.metrics).map(function (protocol) { + var targetHosts = Object.keys(_this.metrics[protocol]).map(function (hostName) { + var host = _this.metrics[protocol][hostName] + return { + name: hostName, + metrics: { + targetServiceKey: host.targetServiceKey, + responseTime: _this.calculateTimes(host.responseTime), + networkDelayIncoming: _this.calculateTimes(host.networkDelayIncoming), + networkDelayOutgoing: _this.calculateTimes(host.networkDelayOutgoing), + status: { + ok: host.status.ok, + notOk: host.status.notOk + } + } + } + }) + + return { + protocol: protocol, + targetHosts: targetHosts + } + }) + + this.metrics = {} + + // if no metrics, don't send anything + if (!metrics || !metrics.length) { + return + } + + this.collectorApi.sendEdgeMetrics({ + timestamp: (new Date()).toISOString(), + hostMetrics: metrics + }) +} + +function create (options) { + return new EdgeMetrics(options) +} + +module.exports.create = create diff --git a/lib/agent/metrics/edge/index.spec.js b/lib/agent/metrics/edge/index.spec.js new file mode 100644 index 0000000..57649e2 --- /dev/null +++ b/lib/agent/metrics/edge/index.spec.js @@ -0,0 +1,127 @@ +var expect = require('chai').expect + +var EdgeMetrics = require('./') + +describe('The EdgeMetrics module', function () { + it('sends metrics', function () { + var ISOString = 'date-string' + var collectorApi = { + sendEdgeMetrics: this.sandbox.spy() + } + + var edgeMetrics = EdgeMetrics.create({ + collectorApi: collectorApi, + config: { + collectInterval: 1 + } + }) + + this.sandbox.stub(Date.prototype, 'toISOString', function () { + return ISOString + }) + + edgeMetrics.report({ + targetHost: 'rstckapp.com', + targetServiceKey: 3, + protocol: 'psql', + networkDelay: { + incoming: 30, + outgoing: 40 + }, + status: 0, + responseTime: 10 + }) + + edgeMetrics.report({ + targetHost: 'rstckapp.com', + targetServiceKey: 3, + protocol: 'psql', + networkDelay: { + incoming: 20, + outgoing: 60 + }, + status: 0, + responseTime: 20 + }) + + edgeMetrics.report({ + targetHost: 'rstckapp.com', + targetServiceKey: 3, + protocol: 'psql', + networkDelay: { + incoming: 1, + outgoing: 3 + }, + status: 0, + responseTime: 3 + }) + + edgeMetrics.report({ + targetHost: 'herokuapp.com', + targetServiceKey: 3, + protocol: 'http', + networkDelay: { + incoming: 20, + outgoing: 40 + }, + status: 1, + responseTime: 10 + }) + + var expectedHostMetrics = [{ + protocol: 'psql', + targetHosts: [{ + name: 'rstckapp.com', + metrics: { + networkDelayIncoming: { + median: 20, + ninetyFive: 30 + }, + networkDelayOutgoing: { + median: 40, + ninetyFive: 60 + }, + responseTime: { + median: 10, + ninetyFive: 20 + }, + status: { + notOk: 0, + ok: 3 + }, + targetServiceKey: 3 + } + }] + }, { + protocol: 'http', + targetHosts: [{ + name: 'herokuapp.com', + metrics: { + networkDelayIncoming: { + median: 20, + ninetyFive: 20 + }, + networkDelayOutgoing: { + median: 40, + ninetyFive: 40 + }, + responseTime: { + median: 10, + ninetyFive: 10 + }, + status: { + notOk: 1, + ok: 0 + }, + targetServiceKey: 3 + } + }] + }] + + edgeMetrics.sendMetrics() + expect(collectorApi.sendEdgeMetrics).to.be.calledWith({ + timestamp: ISOString, + hostMetrics: expectedHostMetrics + }) + }) +}) diff --git a/lib/agent/metrics/index.js b/lib/agent/metrics/index.js index 04e040f..bdacb97 100644 --- a/lib/agent/metrics/index.js +++ b/lib/agent/metrics/index.js @@ -1,2 +1,3 @@ module.exports.apm = require('./apm') module.exports.rpm = require('./rpm') +module.exports.edge = require('./edge') diff --git a/lib/config.js b/lib/config.js index 4f8f874..620a19c 100644 --- a/lib/config.js +++ b/lib/config.js @@ -10,6 +10,7 @@ config.collectorApiSampleEndpoint = '/service/sample' config.collectorApiServiceEndpoint = '/service' config.collectorApiApmMetricsEndpoint = '/service/%s/apm-metrics' config.collectorApiRpmMetricsEndpoint = '/service/%s/rpm-metrics' +config.collectorApiEdgeMetricsEndpoint = '/service/%s/edge-metrics' config.configPath = 'trace.config' diff --git a/lib/consts.js b/lib/consts.js index 97df75a..b55458d 100644 --- a/lib/consts.js +++ b/lib/consts.js @@ -3,6 +3,11 @@ module.exports = { ERROR: '1' }, PROTOCOLS: { - HTTP: 'http' + HTTP: 'http', + MONGODB: 'mongodb' + }, + EDGE_STATUS: { + OK: 0, + NOT_OK: 1 } } diff --git a/lib/instrumentations/core/http/request.js b/lib/instrumentations/core/http/request.js index 202db13..f1d3c12 100644 --- a/lib/instrumentations/core/http/request.js +++ b/lib/instrumentations/core/http/request.js @@ -82,6 +82,8 @@ function wrapRequest (originalHttpRequest, agent, mustCollectStore) { host: requestParams.host, url: util.formatDataUrl(requestParams.path), mustCollect: consts.MUST_COLLECT.ERROR, + protocol: consts.PROTOCOLS.HTTP, + status: consts.EDGE_STATUS.NOT_OK, err: { type: 'network-error', message: err.message, @@ -99,6 +101,10 @@ function wrapRequest (originalHttpRequest, agent, mustCollectStore) { // returns with response returned.on('response', function (incomingMessage) { + var clientReceiveTime = microtime.now() + var networkDelayIncoming + var networkDelayOutgoing + mustCollectStore[requestId] = incomingMessage.headers['x-must-collect'] || mustCollectStore[requestId] @@ -108,6 +114,14 @@ function wrapRequest (originalHttpRequest, agent, mustCollectStore) { debug('trace event (cr) on response; reqId: %s, spanId: %s', requestId, spanId) } + if (incomingMessage.headers['x-server-send']) { + networkDelayIncoming = clientReceiveTime - Number(incomingMessage.headers['x-server-send']) + } + + if (incomingMessage.headers['x-server-receive']) { + networkDelayOutgoing = Number(incomingMessage.headers['x-server-receive']) - clientSendTime + } + var collectorDataBag = { id: requestId, spanId: spanId, @@ -115,7 +129,12 @@ function wrapRequest (originalHttpRequest, agent, mustCollectStore) { host: requestParams.host, url: util.formatDataUrl(requestParams.path), statusCode: incomingMessage.statusCode, - mustCollect: mustCollectStore[requestId] + mustCollect: mustCollectStore[requestId], + targetServiceKey: incomingMessage.headers['x-parent'], + responseTime: clientReceiveTime - clientSendTime, + networkDelayIncoming: networkDelayIncoming, + networkDelayOutgoing: networkDelayOutgoing, + status: incomingMessage.statusCode > 399 ? consts.EDGE_STATUS.NOT_OK : consts.EDGE_STATUS.OK } agent.clientReceive(collectorDataBag) diff --git a/lib/instrumentations/core/http/request.spec.js b/lib/instrumentations/core/http/request.spec.js index ca8f10b..4c6aef0 100644 --- a/lib/instrumentations/core/http/request.spec.js +++ b/lib/instrumentations/core/http/request.spec.js @@ -75,6 +75,7 @@ describe('The http.request wrapper module', function () { } } var r = request(original, agent, mustCollectStore) + var targetServiceKey = 2 r({ host: 'localhost', @@ -84,7 +85,11 @@ describe('The http.request wrapper module', function () { }) cb({ - headers: {}, + headers: { + 'x-parent': targetServiceKey, + 'x-server-send': 12345668, + 'x-server-receive': 12345698 + }, statusCode: 200 }) @@ -106,7 +111,12 @@ describe('The http.request wrapper module', function () { spanId: spanId, statusCode: 200, url: '/', - protocol: 'http' + protocol: 'http', + responseTime: 0, + targetServiceKey: targetServiceKey, + status: 0, + networkDelayIncoming: 10, + networkDelayOutgoing: 20 }) expect(original).to.be.calledWith({ diff --git a/lib/instrumentations/index.js b/lib/instrumentations/index.js index 1c5715f..148d7df 100644 --- a/lib/instrumentations/index.js +++ b/lib/instrumentations/index.js @@ -8,6 +8,7 @@ var shimmer = require('../utils/shimmer') var INSTRUMENTED_LIBS = [ // from npm 'mongoose', + 'mongodb', 'bluebird', 'redis', 'mysql', diff --git a/lib/instrumentations/mongodb.js b/lib/instrumentations/mongodb.js new file mode 100644 index 0000000..3798ed9 --- /dev/null +++ b/lib/instrumentations/mongodb.js @@ -0,0 +1,63 @@ +var Shimmer = require('../utils/shimmer') +var consts = require('../consts') + +var COLLECTION_OPERATIONS = [ + 'find', + 'findOne' +] + +module.exports = function (mongodb, agent) { + Shimmer.wrap(mongodb.Collection.prototype, 'mongodb.Collection.prototype', COLLECTION_OPERATIONS, function (original, name) { + return function () { + var _this = this + var args = Array.prototype.slice.apply(arguments) + var originalCallback = args.pop() + var spanId = agent.generateSpanId() + var clientSendTime = agent.getMicrotime() + var requestId = agent.getTransactionId() + var host + + if (this.db && this.db.serverConfig) { + host = this.db.serverConfig.host + ':' + this.db.serverConfig.port + } else if (this.s && this.s.topology && this.s.topology.isMasterDoc) { + host = this.s.topology.isMasterDoc.primary + } + + var wrapped = function (err) { + agent.clientReceive({ + id: requestId, + spanId: spanId, + protocol: consts.PROTOCOLS.MONGODB, + host: host, + url: _this.collectionName || 'unknown', + method: name, + mustCollect: err ? consts.MUST_COLLECT.ERROR : undefined, + responseTime: agent.getMicrotime() - clientSendTime, + status: err ? consts.EDGE_STATUS.NOT_OK : consts.EDGE_STATUS.OK, + statusCode: err ? err.code : 200 + }) + return originalCallback.apply(this, arguments) + } + + if (originalCallback && typeof originalCallback === 'function') { + args.push(wrapped) + } else { + args.push(originalCallback) + } + + agent.clientSend({ + id: requestId, + spanId: spanId, + host: host, + time: clientSendTime, + method: name, + url: this.collectionName || 'unknown', + type: agent.CLIENT_SEND + }) + + return original.apply(this, args) + } + }) + + return mongodb +} diff --git a/lib/instrumentations/mongodb.spec.js b/lib/instrumentations/mongodb.spec.js new file mode 100644 index 0000000..4ef62d6 --- /dev/null +++ b/lib/instrumentations/mongodb.spec.js @@ -0,0 +1,114 @@ +'use strict' + +var expect = require('chai').expect +var wrapper = require('./mongodb') +var Shimmer = require('../utils/shimmer') + +var COLLECTION_OPERATIONS = [ + 'find', + 'findOne' +] + +describe('The mongodb wrapper module', function () { + it('should wrap collection\'s operations', function () { + var shimmerWrapStub = this.sandbox.stub(Shimmer, 'wrap') + + var fakeMongo = { + Collection: { } + } + + // wrapped as a side effect + wrapper(fakeMongo, null) + + expect(shimmerWrapStub).to.have.been.calledWith( + fakeMongo.Collection.prototype, + 'mongodb.Collection.prototype', + COLLECTION_OPERATIONS + ) + }) + + it('should call clientSend on the Agent when the op is called', function () { + var shimmerWrapStub = this.sandbox.stub(Shimmer, 'wrap') + var collectionName = 'colname' + var operationName = 'find' + var fakeMongo = { + Collection: { } + } + + var fakeAgent = { + generateSpanId: function () { return 'fakeSpanId' }, + getMicrotime: function () { return 42 }, + getTransactionId: function () { return 'fakeTransactionId' }, + clientSend: this.sandbox.spy(), + CLIENT_SEND: 'fakeSend' + } + + // wrapped as a side effect + wrapper(fakeMongo, fakeAgent) + var wrapOp = shimmerWrapStub.args[0][3] + + var fakeMongoOp = this.sandbox.spy() + wrapOp(fakeMongoOp, operationName).bind({ + collectionName: collectionName + })() + + expect(fakeAgent.clientSend).to.have.been.calledOnce + expect(fakeAgent.clientSend).to.have.been.calledWith({ + id: 'fakeTransactionId', + spanId: 'fakeSpanId', + host: undefined, + time: 42, + type: fakeAgent.CLIENT_SEND, + url: collectionName, + method: operationName + }) + }) + + it('should wrap the callback on the op, and call clientReceive when invoked', function () { + var shimmerWrapStub = this.sandbox.stub(Shimmer, 'wrap') + var collectionName = 'colname' + var operationName = 'find' + + var fakeMongo = { + Collection: { } + } + + var fakeAgent = { + generateSpanId: function () { return 'fakeSpanId' }, + getMicrotime: function () { return 42 }, + getTransactionId: function () { return 'fakeTransactionId' }, + clientSend: function () {}, + clientReceive: this.sandbox.spy(), + CLIENT_SEND: 'fakeSend' + } + + // wrapped as a side effect + wrapper(fakeMongo, fakeAgent) + var wrapOp = shimmerWrapStub.args[0][3] + var fakeCallback = this.sandbox.spy() + + var fakeMongoOp = this.sandbox.spy() + wrapOp(fakeMongoOp, operationName).bind({ + collectionName: collectionName + })(fakeCallback) + + var args = fakeMongoOp.args[0] + var wrappedCallback = args[args.length - 1] + + wrappedCallback() + + expect(fakeAgent.clientReceive).to.have.been.calledOnce + expect(fakeAgent.clientReceive).to.have.been.calledWith({ + host: undefined, + mustCollect: undefined, + id: 'fakeTransactionId', + protocol: 'mongodb', + responseTime: 0, + spanId: 'fakeSpanId', + status: 0, + url: collectionName, + method: operationName, + statusCode: 200 + }) + }) +}) diff --git a/lib/instrumentations/mysql.js b/lib/instrumentations/mysql.js index e2c1d66..f974e16 100644 --- a/lib/instrumentations/mysql.js +++ b/lib/instrumentations/mysql.js @@ -1,5 +1,14 @@ var Shimmer = require('../utils/shimmer') +var CONNECTION_OPERATIONS = [ + 'connect', + 'query' +] + +var POOL_OPERATIONS = [ + 'getConnection' +] + module.exports = function (mysql, agent) { var _createConnection = mysql.createConnection var _createPool = mysql.createPool @@ -7,10 +16,7 @@ module.exports = function (mysql, agent) { mysql.createConnection = function (config) { var Connection = _createConnection(config) - Shimmer.wrap(Connection, 'Connection', [ - 'connect', - 'query' - ], function (original) { + Shimmer.wrap(Connection, 'Connection', CONNECTION_OPERATIONS, function (original) { return function () { var args = Array.prototype.slice.apply(arguments) var last = args.length - 1 @@ -30,9 +36,7 @@ module.exports = function (mysql, agent) { mysql.createPool = function (config) { var Pool = _createPool(config) - Shimmer.wrap(Pool, 'Pool', [ - 'getConnection' - ], function (original) { + Shimmer.wrap(Pool, 'Pool', POOL_OPERATIONS, function (original) { return function () { var args = Array.prototype.slice.apply(arguments) var last = args.length - 1 diff --git a/lib/instrumentations/mysql.spec.js b/lib/instrumentations/mysql.spec.js new file mode 100644 index 0000000..84cac8f --- /dev/null +++ b/lib/instrumentations/mysql.spec.js @@ -0,0 +1,83 @@ +'use strict' + +var mysql = require('./mysql') +var Shimmer = require('../utils/shimmer') +var expect = require('chai').expect + +var CONNECTION_OPERATIONS = [ + 'connect', + 'query' +] + +var POOL_OPERATIONS = [ + 'getConnection' +] + +describe('The mysql wrapper', function () { + it('should wrap mysql', function () { + var fakeMysql = { } + + var wrappedFakeMysql = mysql(fakeMysql, null) + + expect(wrappedFakeMysql.createPool).to.be.a('function') + expect(wrappedFakeMysql.createConnection).to.be.a('function') + }) + + it('should wrap Connections', function () { + var shimmerWrapStub = this.sandbox.stub(Shimmer, 'wrap') + + var fakeConfig = { me: 'fakeConfig' } + + var fakeConnection = { me: 'fakeConnection' } + + var createConnectionStub = this.sandbox.stub() + createConnectionStub.returns(fakeConnection) + + var fakeMysql = { + createConnection: createConnectionStub + } + + var wrappedFakeMysql = mysql(fakeMysql, null) + + wrappedFakeMysql.createConnection(fakeConfig) + + expect(createConnectionStub).to.have.been.calledOnce + expect(createConnectionStub).to.have.been.calledWith(fakeConfig) + + expect(shimmerWrapStub).to.have.been.calledOnce + expect(shimmerWrapStub).to.have.been.calledWith( + fakeConnection, + 'Connection', + CONNECTION_OPERATIONS + ) + }) + + it('should wrap Pools', function () { + var shimmerWrapStub = this.sandbox.stub(Shimmer, 'wrap') + + var fakeConfig = { me: 'fakeConfig' } + + var fakePool = { me: 'fakePool' } + + var createPoolStub = this.sandbox.stub() + createPoolStub.returns(fakePool) + + var fakeMysql = { + createPool: createPoolStub + } + + var wrappedFakeMysql = mysql(fakeMysql, null) + + wrappedFakeMysql.createPool(fakeConfig) + + expect(createPoolStub).to.have.been.calledOnce + expect(createPoolStub).to.have.been.calledWith(fakeConfig) + + expect(shimmerWrapStub).to.have.been.calledOnce + expect(shimmerWrapStub).to.have.been.calledWith( + fakePool, + 'Pool', + POOL_OPERATIONS + ) + }) +})