diff --git a/packages/opentelemetry-plugin-mongodb/package.json b/packages/opentelemetry-plugin-mongodb/package.json index 7a7fa290520..de7fb18cf30 100644 --- a/packages/opentelemetry-plugin-mongodb/package.json +++ b/packages/opentelemetry-plugin-mongodb/package.json @@ -39,6 +39,7 @@ "access": "public" }, "devDependencies": { + "@opentelemetry/node": "^0.1.1", "@opentelemetry/tracing": "^0.1.1", "@types/mocha": "^5.2.7", "@types/mongodb": "^3.2.3", @@ -52,13 +53,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.6.3" }, "dependencies": { "@opentelemetry/core": "^0.1.1", - "@opentelemetry/node": "^0.1.1", "@opentelemetry/types": "^0.1.1", "shimmer": "^1.2.1" } diff --git a/packages/opentelemetry-plugin-mongodb/src/mongodb.ts b/packages/opentelemetry-plugin-mongodb/src/mongodb.ts index 985568dcb69..f0f81f8f9a0 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 00000000000..f76cd663ab4 --- /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 616d8ef4186..365a089572a 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();