From 08cff7eab8d1a8bc4351cec32f9fbd062684ebad Mon Sep 17 00:00:00 2001 From: vmarchaud Date: Sat, 17 Aug 2019 17:37:27 +0200 Subject: [PATCH 1/6] feat: add mongodb plugin --- .../opentelemetry-plugin-mongodb/package.json | 7 +- .../src/mongodb.ts | 255 ++++++++++++++++++ 2 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 packages/opentelemetry-plugin-mongodb/src/mongodb.ts diff --git a/packages/opentelemetry-plugin-mongodb/package.json b/packages/opentelemetry-plugin-mongodb/package.json index 6c8b02219d..3bf827d7a1 100644 --- a/packages/opentelemetry-plugin-mongodb/package.json +++ b/packages/opentelemetry-plugin-mongodb/package.json @@ -41,7 +41,9 @@ }, "devDependencies": { "@types/mocha": "^5.2.7", - "@types/node": "^12.6.9", + "@types/mongodb": "^3.2.3", + "@types/node": "^12.7.2", + "@types/shimmer": "^1.0.1", "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", @@ -56,6 +58,7 @@ "dependencies": { "@opentelemetry/core": "^0.2.0", "@opentelemetry/node": "^0.2.0", - "@opentelemetry/types": "^0.2.0" + "@opentelemetry/types": "^0.2.0", + "shimmer": "^1.2.1" } } diff --git a/packages/opentelemetry-plugin-mongodb/src/mongodb.ts b/packages/opentelemetry-plugin-mongodb/src/mongodb.ts new file mode 100644 index 0000000000..276fa9a698 --- /dev/null +++ b/packages/opentelemetry-plugin-mongodb/src/mongodb.ts @@ -0,0 +1,255 @@ +/** + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// mongodb.Server type is deprecated so every use trigger a lint error +/* tslint:disable:deprecation */ + +import { BasePlugin, NoopLogger } from '@opentelemetry/core'; +import { + Span, + SpanKind, + Logger, + Tracer, + CanonicalCode, +} from '@opentelemetry/types'; +import * as mongodb from 'mongodb'; +import * as shimmer from 'shimmer'; + +type Func = (...args: unknown[]) => T; +type MongoDB = typeof mongodb; +interface MongoInternalCommand { + findandmodify: boolean; + createIndexes: boolean; + count: boolean; + ismaster: boolean; +} + +/** MongoDB instrumentation plugin for Opencensus */ +export class MongoDBPlugin extends BasePlugin { + private readonly SERVER_FNS = ['insert', 'update', 'remove', 'auth']; + private readonly CURSOR_FNS_FIRST = ['_find', '_getmore']; + protected _logger!: Logger; + protected readonly _tracer!: Tracer; + + constructor(public moduleName: string) { + super(); + // TODO: remove this once a logger will be passed + // https://github.com/open-telemetry/opentelemetry-js/issues/193 + this._logger = new NoopLogger(); + } + + /** + * Patches MongoDB operations. + */ + protected patch() { + this._logger.debug('Patched MongoDB'); + + if (this._moduleExports.Server) { + this._logger.debug('patching mongodb-core.Server.prototype.command'); + shimmer.wrap( + this._moduleExports.Server.prototype, + 'command' as never, + // tslint:disable-next-line:no-any + this.getPatchCommand() as any + ); + this._logger.debug( + 'patching mongodb-core.Server.prototype functions:', + this.SERVER_FNS + ); + shimmer.massWrap( + [this._moduleExports.Server.prototype], + this.SERVER_FNS as never[], + // tslint:disable-next-line:no-any + this.getPatchQuery() as any + ); + } + + if (this._moduleExports.Cursor) { + this._logger.debug( + 'patching mongodb-core.Cursor.prototype functions:', + this.CURSOR_FNS_FIRST + ); + shimmer.massWrap( + [this._moduleExports.Cursor.prototype], + this.CURSOR_FNS_FIRST as never[], + // tslint:disable-next-line:no-any + this.getPatchCursor() as any + ); + } + + return this._moduleExports; + } + + /** Unpatches all MongoDB patched functions. */ + unpatch(): void { + shimmer.unwrap(this._moduleExports.Server.prototype, 'command' as never); + shimmer.massUnwrap([this._moduleExports.Server.prototype], this + .SERVER_FNS as never[]); + shimmer.massUnwrap([this._moduleExports.Cursor.prototype], this + .CURSOR_FNS_FIRST as never[]); + } + + /** Creates spans for Command operations */ + private getPatchCommand() { + const plugin = this; + return (original: Func) => { + return function( + this: mongodb.Server, + ns: string, + command: MongoInternalCommand, + options: {} | Function, + callback: Func + ): mongodb.Server { + const currentSpan = plugin._tracer.getCurrentSpan(); + if (currentSpan === null) { + return original.apply(this, (arguments as unknown) as unknown[]); + } + const resultHandler = + typeof options === 'function' ? options : callback; + if (typeof resultHandler !== 'function') { + return original.apply(this, (arguments as unknown) as unknown[]); + } + if (typeof command !== 'object') { + return original.apply(this, (arguments as unknown) as unknown[]); + } + let type: string; + if (command.createIndexes) { + type = 'createIndexes'; + } else if (command.findandmodify) { + type = 'findAndModify'; + } else if (command.ismaster) { + type = 'isMaster'; + } else if (command.count) { + type = 'count'; + } else { + type = 'command'; + } + + const span = plugin._tracer.startSpan(`${ns}.${type}`, { + parent: currentSpan, + kind: SpanKind.CLIENT, + }); + if (typeof options === 'function') { + return original.call( + this, + ns, + command, + plugin.patchEnd(span, options as Func) + ); + } else { + return original.call( + this, + ns, + command, + options, + plugin.patchEnd(span, callback) + ); + } + }; + }; + } + + /** Creates spans for Query operations */ + private getPatchQuery() { + const plugin = this; + return (original: Func) => { + return function( + this: mongodb.Server, + ns: string, + command: MongoInternalCommand, + options: {}, + callback: Func + ): mongodb.Server { + const currentSpan = plugin._tracer.getCurrentSpan(); + if (currentSpan === null) { + return original.apply(this, (arguments as unknown) as unknown[]); + } + const resultHandler = + typeof options === 'function' ? options : callback; + if (typeof resultHandler !== 'function') { + return original.apply(this, (arguments as unknown) as unknown[]); + } + const span = plugin._tracer.startSpan(`${ns}.query`, { + kind: SpanKind.CLIENT, + parent: currentSpan, + }); + if (typeof options === 'function') { + return original.call( + this, + ns, + command, + plugin.patchEnd(span, options as Func) + ); + } else { + return original.call( + this, + ns, + command, + options, + plugin.patchEnd(span, callback) + ); + } + }; + }; + } + + /** Creates spans for Cursor operations */ + private getPatchCursor() { + const plugin = this; + return (original: Func) => { + return function( + this: { ns: string }, + ...args: unknown[] + ): mongodb.Cursor { + const currentSpan = plugin._tracer.getCurrentSpan(); + if (currentSpan === null) { + return original.apply(this, (arguments as unknown) as unknown[]); + } + const resultHandler = args[0] as Func | undefined; + if (resultHandler === undefined) { + return original.apply(this, (arguments as unknown) as unknown[]); + } + const span = plugin._tracer.startSpan(`${this.ns}.cursor`, { + parent: currentSpan, + kind: SpanKind.CLIENT, + }); + return original.call(this, plugin.patchEnd(span, resultHandler)); + }; + }; + } + + /** + * Ends a created span. + * @param span The created span to end. + * @param resultHandler A callback function. + */ + patchEnd(span: Span, resultHandler: Func): Function { + return function patchedEnd(this: {}, ...args: unknown[]) { + const error = args[0]; + if (error instanceof Error) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: error.message, + }); + } + span.end(); + return resultHandler.apply(this, (arguments as unknown) as unknown[]); + }; + } +} + +const plugin = new MongoDBPlugin('mongodb'); +export { plugin }; From e35d7fb65e85f8f8a5cf76f5a68a07238d35c330 Mon Sep 17 00:00:00 2001 From: vmarchaud Date: Wed, 21 Aug 2019 23:44:41 +0200 Subject: [PATCH 2/6] test(plugin-mongodb): add tests for mongodb plugin --- .circleci/config.yml | 7 + .../opentelemetry-plugin-mongodb/package.json | 2 + .../opentelemetry-plugin-mongodb/src/index.ts | 2 + .../src/mongodb.ts | 179 ++++------- .../test/mongodb.test.ts | 290 ++++++++++++++++++ 5 files changed, 367 insertions(+), 113 deletions(-) create mode 100644 packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 2e3d019f2e..dac79c43a2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,6 +2,7 @@ version: 2 test_env: &test_env RUN_POSTGRES_TESTS: 1 + RUN_MONGODB_TESTS: 1 POSTGRES_USER: postgres POSTGRES_DB: circle_database POSTGRES_HOST: localhost @@ -13,6 +14,9 @@ postgres_service: &postgres_service POSTGRES_USER: postgres POSTGRES_DB: circle_database +mongo_service: &mongo_service + image: mongo + cache_1: &cache_1 key: npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-1 paths: @@ -145,18 +149,21 @@ jobs: - image: node:8 environment: *test_env - *postgres_service + - *mongo_service <<: *node_unit_tests node10: docker: - image: node:10 environment: *test_env - *postgres_service + - *mongo_service <<: *node_unit_tests node12: docker: - image: node:12 environment: *test_env - *postgres_service + - *mongo_service <<: *node_unit_tests node12-browsers: docker: diff --git a/packages/opentelemetry-plugin-mongodb/package.json b/packages/opentelemetry-plugin-mongodb/package.json index 3bf827d7a1..69c09fdfac 100644 --- a/packages/opentelemetry-plugin-mongodb/package.json +++ b/packages/opentelemetry-plugin-mongodb/package.json @@ -40,6 +40,7 @@ "access": "public" }, "devDependencies": { + "@opentelemetry/tracing": "^0.1.1", "@types/mocha": "^5.2.7", "@types/mongodb": "^3.2.3", "@types/node": "^12.7.2", @@ -47,6 +48,7 @@ "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", + "mongodb": "^3.3.0", "nyc": "^14.1.1", "rimraf": "^3.0.0", "ts-mocha": "^6.0.0", diff --git a/packages/opentelemetry-plugin-mongodb/src/index.ts b/packages/opentelemetry-plugin-mongodb/src/index.ts index ae225f6b52..21a9d57333 100644 --- a/packages/opentelemetry-plugin-mongodb/src/index.ts +++ b/packages/opentelemetry-plugin-mongodb/src/index.ts @@ -13,3 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export * from './mongodb'; diff --git a/packages/opentelemetry-plugin-mongodb/src/mongodb.ts b/packages/opentelemetry-plugin-mongodb/src/mongodb.ts index 276fa9a698..985568dcb6 100644 --- a/packages/opentelemetry-plugin-mongodb/src/mongodb.ts +++ b/packages/opentelemetry-plugin-mongodb/src/mongodb.ts @@ -1,5 +1,21 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** - * Copyright 2018, OpenCensus Authors + * Copyright 2019, OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +33,8 @@ // mongodb.Server type is deprecated so every use trigger a lint error /* tslint:disable:deprecation */ -import { BasePlugin, NoopLogger } from '@opentelemetry/core'; -import { - Span, - SpanKind, - Logger, - Tracer, - CanonicalCode, -} from '@opentelemetry/types'; +import { BasePlugin } from '@opentelemetry/core'; +import { Span, SpanKind, CanonicalCode } from '@opentelemetry/types'; import * as mongodb from 'mongodb'; import * as shimmer from 'shimmer'; @@ -37,18 +47,15 @@ interface MongoInternalCommand { ismaster: boolean; } -/** MongoDB instrumentation plugin for Opencensus */ +/** MongoDB instrumentation plugin for OpenTelemetry */ export class MongoDBPlugin extends BasePlugin { - private readonly SERVER_FNS = ['insert', 'update', 'remove', 'auth']; - private readonly CURSOR_FNS_FIRST = ['_find', '_getmore']; - protected _logger!: Logger; - protected readonly _tracer!: Tracer; + private readonly _SERVER_METHODS = ['insert', 'update', 'remove', 'command']; + private readonly _CURSOR_METHODS = ['_next', 'next']; + + protected readonly _supportedVersions = ['>=2 <3']; - constructor(public moduleName: string) { + constructor(readonly moduleName: string) { super(); - // TODO: remove this once a logger will be passed - // https://github.com/open-telemetry/opentelemetry-js/issues/193 - this._logger = new NoopLogger(); } /** @@ -58,35 +65,29 @@ export class MongoDBPlugin extends BasePlugin { this._logger.debug('Patched MongoDB'); if (this._moduleExports.Server) { - this._logger.debug('patching mongodb-core.Server.prototype.command'); - shimmer.wrap( - this._moduleExports.Server.prototype, - 'command' as never, - // tslint:disable-next-line:no-any - this.getPatchCommand() as any - ); - this._logger.debug( - 'patching mongodb-core.Server.prototype functions:', - this.SERVER_FNS - ); - shimmer.massWrap( - [this._moduleExports.Server.prototype], - this.SERVER_FNS as never[], - // tslint:disable-next-line:no-any - this.getPatchQuery() as any - ); + for (const fn of this._SERVER_METHODS) { + this._logger.debug(`patching mongodb-core.Server.prototype.${fn}`); + shimmer.wrap( + this._moduleExports.Server.prototype, + // Forced to ignore due to incomplete typings + // tslint:disable-next-line:ban-ts-ignore + // @ts-ignore + fn, + this._getPatchCommand(fn) + ); + } } if (this._moduleExports.Cursor) { this._logger.debug( 'patching mongodb-core.Cursor.prototype functions:', - this.CURSOR_FNS_FIRST + this._CURSOR_METHODS ); shimmer.massWrap( [this._moduleExports.Cursor.prototype], - this.CURSOR_FNS_FIRST as never[], + this._CURSOR_METHODS as never[], // tslint:disable-next-line:no-any - this.getPatchCursor() as any + this._getPatchCursor() as any ); } @@ -95,18 +96,17 @@ export class MongoDBPlugin extends BasePlugin { /** Unpatches all MongoDB patched functions. */ unpatch(): void { - shimmer.unwrap(this._moduleExports.Server.prototype, 'command' as never); shimmer.massUnwrap([this._moduleExports.Server.prototype], this - .SERVER_FNS as never[]); + ._SERVER_METHODS as never[]); shimmer.massUnwrap([this._moduleExports.Cursor.prototype], this - .CURSOR_FNS_FIRST as never[]); + ._CURSOR_METHODS as never[]); } /** Creates spans for Command operations */ - private getPatchCommand() { + private _getPatchCommand(operationName: string) { const plugin = this; return (original: Func) => { - return function( + return function patchedServerCommand( this: mongodb.Server, ns: string, command: MongoInternalCommand, @@ -114,84 +114,40 @@ export class MongoDBPlugin extends BasePlugin { callback: Func ): mongodb.Server { const currentSpan = plugin._tracer.getCurrentSpan(); - if (currentSpan === null) { - return original.apply(this, (arguments as unknown) as unknown[]); - } + // @ts-ignore const resultHandler = typeof options === 'function' ? options : callback; - if (typeof resultHandler !== 'function') { - return original.apply(this, (arguments as unknown) as unknown[]); - } - if (typeof command !== 'object') { + if ( + currentSpan === null || + typeof resultHandler !== 'function' || + typeof command !== 'object' + ) { return original.apply(this, (arguments as unknown) as unknown[]); } let type: string; - if (command.createIndexes) { + if (command.createIndexes !== undefined) { type = 'createIndexes'; - } else if (command.findandmodify) { + } else if (command.findandmodify !== undefined) { type = 'findAndModify'; - } else if (command.ismaster) { + } else if (command.ismaster !== undefined) { type = 'isMaster'; - } else if (command.count) { + } else if (command.count !== undefined) { type = 'count'; } else { - type = 'command'; + type = operationName; } - const span = plugin._tracer.startSpan(`${ns}.${type}`, { + const span = plugin._tracer.startSpan(`mongodb.${type}`, { parent: currentSpan, kind: SpanKind.CLIENT, }); + span.setAttribute('db', ns); if (typeof options === 'function') { return original.call( this, ns, command, - plugin.patchEnd(span, options as Func) - ); - } else { - return original.call( - this, - ns, - command, - options, - plugin.patchEnd(span, callback) - ); - } - }; - }; - } - - /** Creates spans for Query operations */ - private getPatchQuery() { - const plugin = this; - return (original: Func) => { - return function( - this: mongodb.Server, - ns: string, - command: MongoInternalCommand, - options: {}, - callback: Func - ): mongodb.Server { - const currentSpan = plugin._tracer.getCurrentSpan(); - if (currentSpan === null) { - return original.apply(this, (arguments as unknown) as unknown[]); - } - const resultHandler = - typeof options === 'function' ? options : callback; - if (typeof resultHandler !== 'function') { - return original.apply(this, (arguments as unknown) as unknown[]); - } - const span = plugin._tracer.startSpan(`${ns}.query`, { - kind: SpanKind.CLIENT, - parent: currentSpan, - }); - if (typeof options === 'function') { - return original.call( - this, - ns, - command, - plugin.patchEnd(span, options as Func) + plugin._patchEnd(span, options as Func) ); } else { return original.call( @@ -199,7 +155,7 @@ export class MongoDBPlugin extends BasePlugin { ns, command, options, - plugin.patchEnd(span, callback) + plugin._patchEnd(span, callback) ); } }; @@ -207,26 +163,24 @@ export class MongoDBPlugin extends BasePlugin { } /** Creates spans for Cursor operations */ - private getPatchCursor() { + private _getPatchCursor() { const plugin = this; return (original: Func) => { - return function( + return function patchedCursorCommand( this: { ns: string }, ...args: unknown[] ): mongodb.Cursor { const currentSpan = plugin._tracer.getCurrentSpan(); - if (currentSpan === null) { - return original.apply(this, (arguments as unknown) as unknown[]); - } const resultHandler = args[0] as Func | undefined; - if (resultHandler === undefined) { + if (currentSpan === null || resultHandler === undefined) { return original.apply(this, (arguments as unknown) as unknown[]); } - const span = plugin._tracer.startSpan(`${this.ns}.cursor`, { + const span = plugin._tracer.startSpan(`mongodb.cursor`, { parent: currentSpan, kind: SpanKind.CLIENT, }); - return original.call(this, plugin.patchEnd(span, resultHandler)); + + return original.call(this, plugin._patchEnd(span, resultHandler)); }; }; } @@ -236,7 +190,7 @@ export class MongoDBPlugin extends BasePlugin { * @param span The created span to end. * @param resultHandler A callback function. */ - patchEnd(span: Span, resultHandler: Func): Function { + private _patchEnd(span: Span, resultHandler: Func): Function { return function patchedEnd(this: {}, ...args: unknown[]) { const error = args[0]; if (error instanceof Error) { @@ -251,5 +205,4 @@ export class MongoDBPlugin extends BasePlugin { } } -const plugin = new MongoDBPlugin('mongodb'); -export { plugin }; +export const plugin = new MongoDBPlugin('mongodb-core'); diff --git a/packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts b/packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts new file mode 100644 index 0000000000..616d8ef418 --- /dev/null +++ b/packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts @@ -0,0 +1,290 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NodeTracer } from '@opentelemetry/node'; +import * as assert from 'assert'; +import * as mongodb from 'mongodb'; +import { plugin } from '../src'; +import { SpanKind } from '@opentelemetry/types'; +import { NoopLogger } from '@opentelemetry/core'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, + ReadableSpan, +} from '@opentelemetry/tracing'; + +export interface MongoDBAccess { + client: mongodb.MongoClient; + collection: mongodb.Collection; +} + +/** + * Access the mongodb collection. + * @param url The mongodb URL to access. + * @param dbName The mongodb database name. + * @param collectionName The mongodb collection name. + */ +function accessCollection( + url: string, + dbName: string, + collectionName: string +): Promise { + return new Promise((resolve, reject) => { + mongodb.MongoClient.connect(url, function connectedClient(err, client) { + if (err) { + reject(err); + return; + } + const db = client.db(dbName); + const collection = db.collection(collectionName); + resolve({ client, collection }); + }); + }); +} + +/** + * Asserts root spans attributes. + * @param rootSpanVerifier An instance of rootSpanVerifier to analyse RootSpan + * instances from. + * @param expectedName The expected name of the first root span. + * @param expectedKind The expected kind of the first root span. + */ +function assertSpans( + spans: ReadableSpan[], + expectedName: string, + expectedKind: SpanKind +) { + assert.strictEqual(spans.length, 2); + spans.forEach(span => { + assert(span.endTime instanceof Array); + assert(span.endTime.length === 2); + }); + assert.strictEqual(spans[0].name, expectedName); + assert.strictEqual(spans[0].kind, expectedKind); +} + +describe('MongoDBPlugin', () => { + // For these tests, mongo must be running. Add OPENTELEMETRY_MONGODB_TESTS to run + // these tests. + const OPENTELEMETRY_MONGODB_TESTS = process.env + .OPENTELEMETRY_MONGODB_TESTS as string; + let shouldTest = true; + if (!OPENTELEMETRY_MONGODB_TESTS) { + console.log('Skipping test-mongodb. Run MongoDB to test'); + shouldTest = false; + } + + const URL = 'mongodb://localhost:27017'; + const DB_NAME = 'opentelemetry-tests'; + const COLLECTION_NAME = 'test'; + + let client: mongodb.MongoClient; + let collection: mongodb.Collection; + const logger = new NoopLogger(); + const tracer = new NodeTracer(); + const memoryExporter = new InMemorySpanExporter(); + tracer.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + + before(done => { + plugin.enable(mongodb, tracer, logger); + accessCollection(URL, DB_NAME, COLLECTION_NAME) + .then(result => { + client = result.client; + collection = result.collection; + done(); + }) + .catch((err: Error) => { + console.log( + 'Skipping test-mongodb. Could not connect. Run MongoDB to test' + ); + shouldTest = false; + done(); + }); + }); + + beforeEach(function mongoBeforeEach(done) { + // Skiping all tests in beforeEach() is a workarround. Mocha does not work + // properly when skiping tests in before() on nested describe() calls. + // https://github.com/mochajs/mocha/issues/2819 + if (!shouldTest) { + this.skip(); + } + memoryExporter.reset(); + // Non traced insertion of basic data to perform tests + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + collection.insertMany(insertData, (err, result) => { + done(); + }); + }); + + afterEach(done => { + collection.deleteOne({}, done); + }); + + after(() => { + if (client) { + client.close(); + } + }); + + /** Should intercept query */ + describe('Instrumenting query operations', () => { + it('should create a child span for insert', done => { + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + + const span = tracer.startSpan(`insertRootSpan`); + tracer.withSpan(span, () => { + collection.insertMany(insertData, (err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.insert`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + + it('should create a child span for update', done => { + const span = tracer.startSpan('updateRootSpan'); + tracer.withSpan(span, () => { + collection.updateOne({ a: 2 }, { $set: { b: 1 } }, (err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.update`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + + it('should create a child span for remove', done => { + const span = tracer.startSpan('removeRootSpan'); + tracer.withSpan(span, () => { + collection.deleteOne({ a: 3 }, (err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.remove`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + }); + + /** Should intercept cursor */ + describe('Instrumenting cursor operations', () => { + it('should create a child span for find', done => { + const span = tracer.startSpan('findRootSpan'); + tracer.withSpan(span, () => { + collection.find({}).toArray((err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.cursor`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + }); + + /** Should intercept command */ + describe('Instrumenting command operations', () => { + it('should create a child span for create index', done => { + const span = tracer.startSpan('indexRootSpan'); + tracer.withSpan(span, () => { + collection.createIndex({ a: 1 }, (err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.createIndexes`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + }); + + /** Should intercept command */ + describe('Removing Instrumentation', () => { + it('should unpatch plugin', () => { + assert.doesNotThrow(() => { + plugin.unpatch(); + }); + }); + + it('should not create a child span for query', done => { + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + + const span = tracer.startSpan('insertRootSpan'); + collection.insertMany(insertData, (err, result) => { + span.end(); + assert.ifError(err); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 1); + done(); + }); + }); + + it('should not create a child span for cursor', done => { + const span = tracer.startSpan('findRootSpan'); + collection.find({}).toArray((err, result) => { + span.end(); + assert.ifError(err); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 1); + done(); + }); + }); + + it('should not create a child span for command', done => { + const span = tracer.startSpan('indexRootSpan'); + collection.createIndex({ a: 1 }, (err, result) => { + span.end(); + assert.ifError(err); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 1); + done(); + }); + }); + }); +}); From a39b4d78241563f3baead43ae435437a337a78e4 Mon Sep 17 00:00:00 2001 From: vmarchaud Date: Sun, 27 Oct 2019 16:16:28 +0100 Subject: [PATCH 3/6] chore(review): address PRs comments / add attributes on each span --- .../opentelemetry-plugin-mongodb/package.json | 4 +- .../src/mongodb.ts | 122 +++++++++++------- .../opentelemetry-plugin-mongodb/src/types.ts | 36 ++++++ .../test/mongodb.test.ts | 21 +-- 4 files changed, 115 insertions(+), 68 deletions(-) create mode 100644 packages/opentelemetry-plugin-mongodb/src/types.ts diff --git a/packages/opentelemetry-plugin-mongodb/package.json b/packages/opentelemetry-plugin-mongodb/package.json index 69c09fdfac..a0c5a5dc94 100644 --- a/packages/opentelemetry-plugin-mongodb/package.json +++ b/packages/opentelemetry-plugin-mongodb/package.json @@ -40,6 +40,7 @@ "access": "public" }, "devDependencies": { + "@opentelemetry/node": "^0.1.1", "@opentelemetry/tracing": "^0.1.1", "@types/mocha": "^5.2.7", "@types/mongodb": "^3.2.3", @@ -53,13 +54,12 @@ "rimraf": "^3.0.0", "ts-mocha": "^6.0.0", "ts-node": "^8.3.0", - "tslint-consistent-codestyle": "^1.15.1", + "tslint-consistent-codestyle": "^1.16.0", "tslint-microsoft-contrib": "^6.2.0", "typescript": "3.7.2" }, "dependencies": { "@opentelemetry/core": "^0.2.0", - "@opentelemetry/node": "^0.2.0", "@opentelemetry/types": "^0.2.0", "shimmer": "^1.2.1" } diff --git a/packages/opentelemetry-plugin-mongodb/src/mongodb.ts b/packages/opentelemetry-plugin-mongodb/src/mongodb.ts index 985568dcb6..f0f81f8f9a 100644 --- a/packages/opentelemetry-plugin-mongodb/src/mongodb.ts +++ b/packages/opentelemetry-plugin-mongodb/src/mongodb.ts @@ -14,45 +14,21 @@ * limitations under the License. */ -/** - * Copyright 2019, OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - // mongodb.Server type is deprecated so every use trigger a lint error /* tslint:disable:deprecation */ import { BasePlugin } from '@opentelemetry/core'; import { Span, SpanKind, CanonicalCode } from '@opentelemetry/types'; +import { Func, MongoInternalCommand, MongoInternalTopology } from './types'; import * as mongodb from 'mongodb'; import * as shimmer from 'shimmer'; -type Func = (...args: unknown[]) => T; -type MongoDB = typeof mongodb; -interface MongoInternalCommand { - findandmodify: boolean; - createIndexes: boolean; - count: boolean; - ismaster: boolean; -} - /** MongoDB instrumentation plugin for OpenTelemetry */ -export class MongoDBPlugin extends BasePlugin { +export class MongoDBPlugin extends BasePlugin { private readonly _SERVER_METHODS = ['insert', 'update', 'remove', 'command']; private readonly _CURSOR_METHODS = ['_next', 'next']; - protected readonly _supportedVersions = ['>=2 <3']; + readonly supportedVersions = ['>=2 <3']; constructor(readonly moduleName: string) { super(); @@ -109,7 +85,7 @@ export class MongoDBPlugin extends BasePlugin { return function patchedServerCommand( this: mongodb.Server, ns: string, - command: MongoInternalCommand, + commands: MongoInternalCommand[] | MongoInternalCommand, options: {} | Function, callback: Func ): mongodb.Server { @@ -120,40 +96,34 @@ export class MongoDBPlugin extends BasePlugin { if ( currentSpan === null || typeof resultHandler !== 'function' || - typeof command !== 'object' + typeof commands !== 'object' ) { return original.apply(this, (arguments as unknown) as unknown[]); } - let type: string; - if (command.createIndexes !== undefined) { - type = 'createIndexes'; - } else if (command.findandmodify !== undefined) { - type = 'findAndModify'; - } else if (command.ismaster !== undefined) { - type = 'isMaster'; - } else if (command.count !== undefined) { - type = 'count'; - } else { - type = operationName; - } - + const command = commands instanceof Array ? commands[0] : commands; + const type = plugin._getCommandType(command, operationName); const span = plugin._tracer.startSpan(`mongodb.${type}`, { parent: currentSpan, kind: SpanKind.CLIENT, }); - span.setAttribute('db', ns); + plugin._populateAttributes( + span, + ns, + command, + this as MongoInternalTopology + ); if (typeof options === 'function') { return original.call( this, ns, - command, + commands, plugin._patchEnd(span, options as Func) ); } else { return original.call( this, ns, - command, + commands, options, plugin._patchEnd(span, callback) ); @@ -162,12 +132,69 @@ export class MongoDBPlugin extends BasePlugin { }; } + /** + * Get the mongodb command type from the object. + * @param command Internal mongodb command object + * @param defaulType the default type to return if we could not find a + * specific command. + */ + private _getCommandType(command: MongoInternalCommand, defaulType: string) { + if (command.createIndexes !== undefined) { + return 'createIndexes'; + } else if (command.findandmodify !== undefined) { + return 'findAndModify'; + } else if (command.ismaster !== undefined) { + return 'isMaster'; + } else if (command.count !== undefined) { + return 'count'; + } else { + return defaulType; + } + } + + /** + * Populate span's attributes by fetching related metadata from the context + * @param span span to add attributes to + * @param ns mongodb namespace + * @param command mongodb internal representation of a command + * @param topology mongodb internal representation of the network topology + */ + private _populateAttributes( + span: Span, + ns: string, + command: MongoInternalCommand, + topology: MongoInternalTopology + ) { + // add network attributes to determine the remote server + if (topology && topology.s && topology.s.options) { + span.setAttribute('peer.hostname', `${topology.s.options.host}`); + span.setAttribute('peer.port', `${topology.s.options.port}`); + } + // add database related attributes + span.setAttribute('db.instance', `${ns}`); + span.setAttribute('db.type', `mongodb`); + span.setAttribute('component', 'mongodb-core'); + if (command === undefined) return; + const query = Object.keys(command.query || command.q || {}).reduce( + (obj, key) => { + obj[key] = '?'; + return obj; + }, + {} as { [key: string]: string } + ); + span.setAttribute('db.statement', JSON.stringify(query)); + } + /** Creates spans for Cursor operations */ private _getPatchCursor() { const plugin = this; return (original: Func) => { return function patchedCursorCommand( - this: { ns: string }, + this: { + ns: string; + cmd: MongoInternalCommand; + topology: MongoInternalTopology; + }, ...args: unknown[] ): mongodb.Cursor { const currentSpan = plugin._tracer.getCurrentSpan(); @@ -175,10 +202,11 @@ export class MongoDBPlugin extends BasePlugin { if (currentSpan === null || resultHandler === undefined) { return original.apply(this, (arguments as unknown) as unknown[]); } - const span = plugin._tracer.startSpan(`mongodb.cursor`, { + const span = plugin._tracer.startSpan(`mongodb.query`, { parent: currentSpan, kind: SpanKind.CLIENT, }); + plugin._populateAttributes(span, this.ns, this.cmd, this.topology); return original.call(this, plugin._patchEnd(span, resultHandler)); }; diff --git a/packages/opentelemetry-plugin-mongodb/src/types.ts b/packages/opentelemetry-plugin-mongodb/src/types.ts new file mode 100644 index 0000000000..f76cd663ab --- /dev/null +++ b/packages/opentelemetry-plugin-mongodb/src/types.ts @@ -0,0 +1,36 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type Func = (...args: unknown[]) => T; +export type MongoInternalCommand = { + findandmodify: boolean; + createIndexes: boolean; + count: boolean; + ismaster: boolean; + query?: { [key: string]: unknown }; + q?: { [key: string]: unknown }; +}; +// +// https://github.com/mongodb-js/mongodb-core/blob/master/lib/topologies/server.js#L117 +export type MongoInternalTopology = { + s?: { + options?: { + host?: string; + port?: number; + servername?: string; + }; + }; +}; diff --git a/packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts b/packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts index 616d8ef418..365a089572 100644 --- a/packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts +++ b/packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts @@ -14,22 +14,6 @@ * limitations under the License. */ -/** - * Copyright 2019, OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { NodeTracer } from '@opentelemetry/node'; import * as assert from 'assert'; import * as mongodb from 'mongodb'; @@ -73,8 +57,7 @@ function accessCollection( /** * Asserts root spans attributes. - * @param rootSpanVerifier An instance of rootSpanVerifier to analyse RootSpan - * instances from. + * @param spans Readable spans that we need to asert. * @param expectedName The expected name of the first root span. * @param expectedKind The expected kind of the first root span. */ @@ -219,7 +202,7 @@ describe('MongoDBPlugin', () => { assert.ifError(err); assertSpans( memoryExporter.getFinishedSpans(), - `mongodb.cursor`, + `mongodb.query`, SpanKind.CLIENT ); done(); From 7f203f49c93b773251542201551410e1c5a3394d Mon Sep 17 00:00:00 2001 From: vmarchaud Date: Sun, 27 Oct 2019 16:39:50 +0100 Subject: [PATCH 4/6] chore: rename mongodb plugin to mongodb-core --- .../.npmignore | 0 .../LICENSE | 0 .../README.md | 16 ++++++++-------- .../package.json | 6 +++--- .../src/index.ts | 0 .../src/mongodb.ts | 0 .../src/types.ts | 0 .../test/mongodb.test.ts | 0 .../tsconfig.json | 0 .../tslint.json | 0 10 files changed, 11 insertions(+), 11 deletions(-) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/.npmignore (100%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/LICENSE (100%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/README.md (77%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/package.json (90%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/src/index.ts (100%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/src/mongodb.ts (100%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/src/types.ts (100%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/test/mongodb.test.ts (100%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/tsconfig.json (100%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/tslint.json (100%) diff --git a/packages/opentelemetry-plugin-mongodb/.npmignore b/packages/opentelemetry-plugin-mongodb-core/.npmignore similarity index 100% rename from packages/opentelemetry-plugin-mongodb/.npmignore rename to packages/opentelemetry-plugin-mongodb-core/.npmignore diff --git a/packages/opentelemetry-plugin-mongodb/LICENSE b/packages/opentelemetry-plugin-mongodb-core/LICENSE similarity index 100% rename from packages/opentelemetry-plugin-mongodb/LICENSE rename to packages/opentelemetry-plugin-mongodb-core/LICENSE diff --git a/packages/opentelemetry-plugin-mongodb/README.md b/packages/opentelemetry-plugin-mongodb-core/README.md similarity index 77% rename from packages/opentelemetry-plugin-mongodb/README.md rename to packages/opentelemetry-plugin-mongodb-core/README.md index 6c05d1b0da..1c6ed4aac6 100644 --- a/packages/opentelemetry-plugin-mongodb/README.md +++ b/packages/opentelemetry-plugin-mongodb-core/README.md @@ -1,10 +1,10 @@ -# OpenTelemetry mongodb Instrumentation for Node.js +# OpenTelemetry mongodb-core Instrumentation for Node.js [![Gitter chat][gitter-image]][gitter-url] [![dependencies][dependencies-image]][dependencies-url] [![devDependencies][devDependencies-image]][devDependencies-url] [![Apache License][license-image]][license-image] -This module provides automatic instrumentation for [`mongodb`](https://github.com/mongodb/node-mongodb-native). +This module provides automatic instrumentation for [`mongodb-core`](https://github.com/mongodb-js/mongodb-core). For automatic instrumentation see the [@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node) package. @@ -12,13 +12,13 @@ For automatic instrumentation see the ## Installation ```bash -npm install --save @opentelemetry/plugin-mongodb +npm install --save @opentelemetry/plugin-mongodb-core ``` ## Usage ```js -const opentelemetry = require('@opentelemetry/plugin-mongodb'); +const opentelemetry = require('@opentelemetry/plugin-mongodb-core'); // TODO: DEMONSTRATE API ``` @@ -36,7 +36,7 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge [license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE [license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat -[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-plugin-mongodb -[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-mongodb -[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-plugin-mongodb -[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-mongodb&type=dev +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-plugin-mongodb-core +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-mongodb-core +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-plugin-mongodb-core +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-mongodb-core&type=dev diff --git a/packages/opentelemetry-plugin-mongodb/package.json b/packages/opentelemetry-plugin-mongodb-core/package.json similarity index 90% rename from packages/opentelemetry-plugin-mongodb/package.json rename to packages/opentelemetry-plugin-mongodb-core/package.json index a0c5a5dc94..f54d725add 100644 --- a/packages/opentelemetry-plugin-mongodb/package.json +++ b/packages/opentelemetry-plugin-mongodb-core/package.json @@ -1,7 +1,7 @@ { - "name": "@opentelemetry/plugin-mongodb", + "name": "@opentelemetry/plugin-mongodb-core", "version": "0.2.0", - "description": "OpenTelemetry mongodb automatic instrumentation package.", + "description": "OpenTelemetry mongodb-core automatic instrumentation package.", "private": true, "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -18,7 +18,7 @@ }, "keywords": [ "opentelemetry", - "mongodb", + "mongodb-core", "nodejs", "tracing", "profiling", diff --git a/packages/opentelemetry-plugin-mongodb/src/index.ts b/packages/opentelemetry-plugin-mongodb-core/src/index.ts similarity index 100% rename from packages/opentelemetry-plugin-mongodb/src/index.ts rename to packages/opentelemetry-plugin-mongodb-core/src/index.ts diff --git a/packages/opentelemetry-plugin-mongodb/src/mongodb.ts b/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts similarity index 100% rename from packages/opentelemetry-plugin-mongodb/src/mongodb.ts rename to packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts diff --git a/packages/opentelemetry-plugin-mongodb/src/types.ts b/packages/opentelemetry-plugin-mongodb-core/src/types.ts similarity index 100% rename from packages/opentelemetry-plugin-mongodb/src/types.ts rename to packages/opentelemetry-plugin-mongodb-core/src/types.ts diff --git a/packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts b/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts similarity index 100% rename from packages/opentelemetry-plugin-mongodb/test/mongodb.test.ts rename to packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts diff --git a/packages/opentelemetry-plugin-mongodb/tsconfig.json b/packages/opentelemetry-plugin-mongodb-core/tsconfig.json similarity index 100% rename from packages/opentelemetry-plugin-mongodb/tsconfig.json rename to packages/opentelemetry-plugin-mongodb-core/tsconfig.json diff --git a/packages/opentelemetry-plugin-mongodb/tslint.json b/packages/opentelemetry-plugin-mongodb-core/tslint.json similarity index 100% rename from packages/opentelemetry-plugin-mongodb/tslint.json rename to packages/opentelemetry-plugin-mongodb-core/tslint.json From a95c2cc342c91643b94b9eb02ee05a8a66b634d9 Mon Sep 17 00:00:00 2001 From: vmarchaud Date: Sun, 3 Nov 2019 18:15:17 +0100 Subject: [PATCH 5/6] chore(review): fix CI tests + use enum for attributes names --- .circleci/config.yml | 3 ++ .../src/mongodb.ts | 38 ++++++++++++++----- .../src/types.ts | 15 ++++++++ .../test/mongodb.test.ts | 29 +++++++++----- .../tsconfig.json | 3 +- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index dac79c43a2..e0bcbee841 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,6 +7,9 @@ test_env: &test_env POSTGRES_DB: circle_database POSTGRES_HOST: localhost POSTGRES_PORT: 5432 + MONGODB_HOST: localhost + MONGODB_PORT: 27017 + MONGODB_DB: opentelemetry-tests postgres_service: &postgres_service image: circleci/postgres:9.6-alpine diff --git a/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts b/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts index f0f81f8f9a..070a3c8018 100644 --- a/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts +++ b/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts @@ -19,7 +19,12 @@ import { BasePlugin } from '@opentelemetry/core'; import { Span, SpanKind, CanonicalCode } from '@opentelemetry/types'; -import { Func, MongoInternalCommand, MongoInternalTopology } from './types'; +import { + Func, + MongoInternalCommand, + MongoInternalTopology, + AttributeNames, +} from './types'; import * as mongodb from 'mongodb'; import * as shimmer from 'shimmer'; @@ -72,10 +77,14 @@ export class MongoDBPlugin extends BasePlugin { /** Unpatches all MongoDB patched functions. */ unpatch(): void { - shimmer.massUnwrap([this._moduleExports.Server.prototype], this - ._SERVER_METHODS as never[]); - shimmer.massUnwrap([this._moduleExports.Cursor.prototype], this - ._CURSOR_METHODS as never[]); + shimmer.massUnwrap( + [this._moduleExports.Server.prototype], + this._SERVER_METHODS as never[] + ); + shimmer.massUnwrap( + [this._moduleExports.Cursor.prototype], + this._CURSOR_METHODS as never[] + ); } /** Creates spans for Command operations */ @@ -167,13 +176,18 @@ export class MongoDBPlugin extends BasePlugin { ) { // add network attributes to determine the remote server if (topology && topology.s && topology.s.options) { - span.setAttribute('peer.hostname', `${topology.s.options.host}`); - span.setAttribute('peer.port', `${topology.s.options.port}`); + span.setAttributes({ + [AttributeNames.PEER_HOSTNAME]: `${topology.s.options.host}`, + [AttributeNames.PEER_PORT]: `${topology.s.options.port}`, + }); } // add database related attributes - span.setAttribute('db.instance', `${ns}`); - span.setAttribute('db.type', `mongodb`); - span.setAttribute('component', 'mongodb-core'); + span.setAttributes({ + [AttributeNames.DB_INSTANCE]: `${ns}`, + [AttributeNames.DB_TYPE]: `mongodb`, + [AttributeNames.COMPONENT]: 'mongodb-core', + }); + if (command === undefined) return; const query = Object.keys(command.query || command.q || {}).reduce( (obj, key) => { @@ -226,6 +240,10 @@ export class MongoDBPlugin extends BasePlugin { code: CanonicalCode.UNKNOWN, message: error.message, }); + } else { + span.setStatus({ + code: CanonicalCode.OK, + }); } span.end(); return resultHandler.apply(this, (arguments as unknown) as unknown[]); diff --git a/packages/opentelemetry-plugin-mongodb-core/src/types.ts b/packages/opentelemetry-plugin-mongodb-core/src/types.ts index f76cd663ab..4763f662d5 100644 --- a/packages/opentelemetry-plugin-mongodb-core/src/types.ts +++ b/packages/opentelemetry-plugin-mongodb-core/src/types.ts @@ -34,3 +34,18 @@ export type MongoInternalTopology = { }; }; }; + +export enum AttributeNames { + // required by https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#databases-client-calls + COMPONENT = 'component', + DB_TYPE = 'db.type', + DB_INSTANCE = 'db.instance', + DB_STATEMENT = 'db.statement', + PEER_ADDRESS = 'peer.address', + PEER_HOSTNAME = 'peer.host', + + PEER_PORT = 'peer.port', + PEER_IPV4 = 'peer.ipv4', + PEER_IPV6 = 'peer.ipv6', + PEER_SERVICE = 'peer.service', +} diff --git a/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts b/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts index 365a089572..522532a0b2 100644 --- a/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts +++ b/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts @@ -18,8 +18,9 @@ import { NodeTracer } from '@opentelemetry/node'; import * as assert from 'assert'; import * as mongodb from 'mongodb'; import { plugin } from '../src'; -import { SpanKind } from '@opentelemetry/types'; +import { SpanKind, CanonicalCode } from '@opentelemetry/types'; import { NoopLogger } from '@opentelemetry/core'; +import { AttributeNames } from '../src/types'; import { InMemorySpanExporter, SimpleSpanProcessor, @@ -71,23 +72,33 @@ function assertSpans( assert(span.endTime instanceof Array); assert(span.endTime.length === 2); }); - assert.strictEqual(spans[0].name, expectedName); - assert.strictEqual(spans[0].kind, expectedKind); + const [mongoSpan] = spans; + assert.strictEqual(mongoSpan.name, expectedName); + assert.strictEqual(mongoSpan.kind, expectedKind); + assert.strictEqual( + mongoSpan.attributes[AttributeNames.COMPONENT], + 'mongodb-core' + ); + assert.strictEqual( + mongoSpan.attributes[AttributeNames.PEER_HOSTNAME], + process.env.MONGODB_HOST || 'localhost' + ); + assert.strictEqual(mongoSpan.status.code, CanonicalCode.OK); } describe('MongoDBPlugin', () => { - // For these tests, mongo must be running. Add OPENTELEMETRY_MONGODB_TESTS to run + // For these tests, mongo must be running. Add RUN_MONGODB_TESTS to run // these tests. - const OPENTELEMETRY_MONGODB_TESTS = process.env - .OPENTELEMETRY_MONGODB_TESTS as string; + const RUN_MONGODB_TESTS = process.env.RUN_MONGODB_TESTS as string; let shouldTest = true; - if (!OPENTELEMETRY_MONGODB_TESTS) { + if (!RUN_MONGODB_TESTS) { console.log('Skipping test-mongodb. Run MongoDB to test'); shouldTest = false; } - const URL = 'mongodb://localhost:27017'; - const DB_NAME = 'opentelemetry-tests'; + const URL = `mongodb://${process.env.MONGODB_HOST || 'localhost'}:${process + .env.MONGODB_PORT || '27017'}`; + const DB_NAME = process.env.MONGODB_DB || 'opentelemetry-tests'; const COLLECTION_NAME = 'test'; let client: mongodb.MongoClient; diff --git a/packages/opentelemetry-plugin-mongodb-core/tsconfig.json b/packages/opentelemetry-plugin-mongodb-core/tsconfig.json index a2042cd68b..3e83278f6c 100644 --- a/packages/opentelemetry-plugin-mongodb-core/tsconfig.json +++ b/packages/opentelemetry-plugin-mongodb-core/tsconfig.json @@ -5,7 +5,6 @@ "outDir": "build" }, "include": [ - "src/**/*.ts", - "test/**/*.ts" + "src/**/*.ts" ] } From 4def83ed8ce8643f2d71fe624684328b643dcdad Mon Sep 17 00:00:00 2001 From: vmarchaud Date: Fri, 15 Nov 2019 13:51:54 +0100 Subject: [PATCH 6/6] chore: address PR comments --- .circleci/config.yml | 2 +- .../src/mongodb.ts | 67 +++++++++---------- .../src/types.ts | 8 +++ .../test/mongodb.test.ts | 2 +- 4 files changed, 42 insertions(+), 37 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e0bcbee841..95d4c61fa2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,7 +46,7 @@ cache_2: &cache_2 - packages/opentelemetry-plugin-grpc/node_modules - packages/opentelemetry-plugin-http/node_modules - packages/opentelemetry-plugin-http2/node_modules - - packages/opentelemetry-plugin-mongodb/node_modules + - packages/opentelemetry-plugin-mongodb-core/node_modules - packages/opentelemetry-plugin-redis/node_modules - packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/node_modules - packages/opentelemetry-plugin-document-load/node_modules diff --git a/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts b/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts index 070a3c8018..e3e179c7b2 100644 --- a/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts +++ b/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts @@ -24,15 +24,19 @@ import { MongoInternalCommand, MongoInternalTopology, AttributeNames, + MongodbCommandType, } from './types'; import * as mongodb from 'mongodb'; import * as shimmer from 'shimmer'; -/** MongoDB instrumentation plugin for OpenTelemetry */ -export class MongoDBPlugin extends BasePlugin { +/** MongoDBCore instrumentation plugin for OpenTelemetry */ +export class MongoDBCorePlugin extends BasePlugin { private readonly _SERVER_METHODS = ['insert', 'update', 'remove', 'command']; private readonly _CURSOR_METHODS = ['_next', 'next']; + private readonly _COMPONENT = 'mongodb-core'; + private readonly _DB_TYPE = 'mongodb'; + readonly supportedVersions = ['>=2 <3']; constructor(readonly moduleName: string) { @@ -96,10 +100,9 @@ export class MongoDBPlugin extends BasePlugin { ns: string, commands: MongoInternalCommand[] | MongoInternalCommand, options: {} | Function, - callback: Func + callback: Function ): mongodb.Server { const currentSpan = plugin._tracer.getCurrentSpan(); - // @ts-ignore const resultHandler = typeof options === 'function' ? options : callback; if ( @@ -110,7 +113,11 @@ export class MongoDBPlugin extends BasePlugin { return original.apply(this, (arguments as unknown) as unknown[]); } const command = commands instanceof Array ? commands[0] : commands; - const type = plugin._getCommandType(command, operationName); + const commandType = plugin._getCommandType(command); + const type = + commandType === MongodbCommandType.UNKNOWN + ? operationName + : commandType; const span = plugin._tracer.startSpan(`mongodb.${type}`, { parent: currentSpan, kind: SpanKind.CLIENT, @@ -121,22 +128,12 @@ export class MongoDBPlugin extends BasePlugin { command, this as MongoInternalTopology ); - if (typeof options === 'function') { - return original.call( - this, - ns, - commands, - plugin._patchEnd(span, options as Func) - ); - } else { - return original.call( - this, - ns, - commands, - options, - plugin._patchEnd(span, callback) - ); - } + return original.call( + this, + ns, + commands, + plugin._patchEnd(span, resultHandler) + ); }; }; } @@ -147,17 +144,17 @@ export class MongoDBPlugin extends BasePlugin { * @param defaulType the default type to return if we could not find a * specific command. */ - private _getCommandType(command: MongoInternalCommand, defaulType: string) { + private _getCommandType(command: MongoInternalCommand): MongodbCommandType { if (command.createIndexes !== undefined) { - return 'createIndexes'; + return MongodbCommandType.CREATE_INDEXES; } else if (command.findandmodify !== undefined) { - return 'findAndModify'; + return MongodbCommandType.FIND_AND_MODIFY; } else if (command.ismaster !== undefined) { - return 'isMaster'; + return MongodbCommandType.IS_MASTER; } else if (command.count !== undefined) { - return 'count'; + return MongodbCommandType.COUNT; } else { - return defaulType; + return MongodbCommandType.UNKNOWN; } } @@ -184,8 +181,8 @@ export class MongoDBPlugin extends BasePlugin { // add database related attributes span.setAttributes({ [AttributeNames.DB_INSTANCE]: `${ns}`, - [AttributeNames.DB_TYPE]: `mongodb`, - [AttributeNames.COMPONENT]: 'mongodb-core', + [AttributeNames.DB_TYPE]: this._DB_TYPE, + [AttributeNames.COMPONENT]: this._COMPONENT, }); if (command === undefined) return; @@ -212,9 +209,9 @@ export class MongoDBPlugin extends BasePlugin { ...args: unknown[] ): mongodb.Cursor { const currentSpan = plugin._tracer.getCurrentSpan(); - const resultHandler = args[0] as Func | undefined; - if (currentSpan === null || resultHandler === undefined) { - return original.apply(this, (arguments as unknown) as unknown[]); + const resultHandler = args[0]; + if (currentSpan === null || typeof resultHandler !== 'function') { + return original.apply(this, args); } const span = plugin._tracer.startSpan(`mongodb.query`, { parent: currentSpan, @@ -232,7 +229,7 @@ export class MongoDBPlugin extends BasePlugin { * @param span The created span to end. * @param resultHandler A callback function. */ - private _patchEnd(span: Span, resultHandler: Func): Function { + private _patchEnd(span: Span, resultHandler: Function): Function { return function patchedEnd(this: {}, ...args: unknown[]) { const error = args[0]; if (error instanceof Error) { @@ -246,9 +243,9 @@ export class MongoDBPlugin extends BasePlugin { }); } span.end(); - return resultHandler.apply(this, (arguments as unknown) as unknown[]); + return resultHandler.apply(this, args); }; } } -export const plugin = new MongoDBPlugin('mongodb-core'); +export const plugin = new MongoDBCorePlugin('mongodb-core'); diff --git a/packages/opentelemetry-plugin-mongodb-core/src/types.ts b/packages/opentelemetry-plugin-mongodb-core/src/types.ts index 4763f662d5..3c8b33b542 100644 --- a/packages/opentelemetry-plugin-mongodb-core/src/types.ts +++ b/packages/opentelemetry-plugin-mongodb-core/src/types.ts @@ -49,3 +49,11 @@ export enum AttributeNames { PEER_IPV6 = 'peer.ipv6', PEER_SERVICE = 'peer.service', } + +export enum MongodbCommandType { + CREATE_INDEXES = 'createIndexes', + FIND_AND_MODIFY = 'findAndModify', + IS_MASTER = 'isMaster', + COUNT = 'count', + UNKNOWN = 'unknown', +} diff --git a/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts b/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts index 522532a0b2..b0f4ee15fc 100644 --- a/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts +++ b/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts @@ -27,7 +27,7 @@ import { ReadableSpan, } from '@opentelemetry/tracing'; -export interface MongoDBAccess { +interface MongoDBAccess { client: mongodb.MongoClient; collection: mongodb.Collection; }