diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/examples/package.json b/plugins/node/opentelemetry-instrumentation-mongodb/examples/package.json index 58375fd960f..57883bace5c 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/examples/package.json +++ b/plugins/node/opentelemetry-instrumentation-mongodb/examples/package.json @@ -38,7 +38,7 @@ "@opentelemetry/instrumentation-mongodb": "^0.32.0", "@opentelemetry/sdk-trace-node": "^1.0.0", "@opentelemetry/sdk-trace-base": "^1.0.0", - "mongodb": "^3.5.7" + "mongodb": "^3.7.3" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib#readme", "devDependencies": { diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/package.json b/plugins/node/opentelemetry-instrumentation-mongodb/package.json index 22947a11518..2c5a13e5e83 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/package.json +++ b/plugins/node/opentelemetry-instrumentation-mongodb/package.json @@ -8,7 +8,7 @@ "scripts": { "docker:start": "docker run -e MONGODB_DB=opentelemetry-tests -e MONGODB_PORT=27017 -e MONGODB_HOST=127.0.0.1 -p 27017:27017 --rm mongo", "test": "nyc ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/**/mongodb-v3.test.ts'", - "test-new-versions": "nyc ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/**mongodb-v4.test.ts'", + "test-new-versions": "nyc ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/mongodb-v4**.test.ts'", "test-all-versions": "tav", "tdd": "npm run test -- --watch-extensions ts --watch", "clean": "rimraf build/*", @@ -68,6 +68,7 @@ "typescript": "4.4.4" }, "dependencies": { + "@opentelemetry/sdk-metrics": "^1.9.1", "@opentelemetry/instrumentation": "^0.39.1", "@opentelemetry/semantic-conventions": "^1.0.0" }, diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts index 8361de772c4..f1f66f8f49d 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts @@ -36,23 +36,47 @@ import { import { MongoDBInstrumentationConfig, CommandResult } from './types'; import { CursorState, + ServerSession, MongodbCommandType, MongoInternalCommand, MongoInternalTopology, WireProtocolInternal, V4Connection, } from './internal-types'; +import { V4Connect, V4Session } from './internal-types'; import { VERSION } from './version'; +import { UpDownCounter } from '@opentelemetry/api'; /** mongodb instrumentation plugin for OpenTelemetry */ export class MongoDBInstrumentation extends InstrumentationBase { + private _connectionsUsage!: UpDownCounter; + private _poolName!: string; + constructor(protected override _config: MongoDBInstrumentationConfig = {}) { super('@opentelemetry/instrumentation-mongodb', VERSION, _config); } + override _updateMetricInstruments() { + this._connectionsUsage = this.meter.createUpDownCounter( + 'db.client.connections.usage', + { + description: + 'The number of connections that are currently in state described by the state attribute.', + unit: '{connection}', + } + ); + } + init() { - const { v3Patch, v3Unpatch } = this._getV3Patches(); - const { v4Patch, v4Unpatch } = this._getV4Patches(); + const { + v3PatchConnection: v3PatchConnection, + v3UnpatchConnection: v3UnpatchConnection, + } = this._getV3ConnectionPatches(); + + const { v4PatchConnect, v4UnpatchConnect } = this._getV4ConnectPatches(); + const { v4PatchConnection, v4UnpatchConnection } = + this._getV4ConnectionPatches(); + const { v4PatchSessions, v4UnpatchSessions } = this._getV4SessionsPatches(); return [ new InstrumentationNodeModuleDefinition( @@ -64,8 +88,8 @@ export class MongoDBInstrumentation extends InstrumentationBase { new InstrumentationNodeModuleFile( 'mongodb/lib/core/wireprotocol/index.js', ['>=3.3 <4'], - v3Patch, - v3Unpatch + v3PatchConnection, + v3UnpatchConnection ), ] ), @@ -78,17 +102,29 @@ export class MongoDBInstrumentation extends InstrumentationBase { new InstrumentationNodeModuleFile( 'mongodb/lib/cmap/connection.js', ['4.*'], - v4Patch, - v4Unpatch + v4PatchConnection, + v4UnpatchConnection + ), + new InstrumentationNodeModuleFile( + 'mongodb/lib/cmap/connect.js', + ['4.*'], + v4PatchConnect, + v4UnpatchConnect + ), + new InstrumentationNodeModuleFile( + 'mongodb/lib/sessions.js', + ['4.*'], + v4PatchSessions, + v4UnpatchSessions ), ] ), ]; } - private _getV3Patches() { + private _getV3ConnectionPatches() { return { - v3Patch: (moduleExports: T, moduleVersion?: string) => { + v3PatchConnection: (moduleExports: T, moduleVersion?: string) => { diag.debug(`Applying patch for mongodb@${moduleVersion}`); // patch insert operation if (isWrapped(moduleExports.insert)) { @@ -134,7 +170,7 @@ export class MongoDBInstrumentation extends InstrumentationBase { this._wrap(moduleExports, 'getMore', this._getV3PatchCursor()); return moduleExports; }, - v3Unpatch: (moduleExports?: T, moduleVersion?: string) => { + v3UnpatchConnection: (moduleExports?: T, moduleVersion?: string) => { if (moduleExports === undefined) return; diag.debug(`Removing internal patch for mongodb@${moduleVersion}`); this._unwrap(moduleExports, 'insert'); @@ -147,10 +183,137 @@ export class MongoDBInstrumentation extends InstrumentationBase { }; } + private _getV4SessionsPatches() { + return { + v4PatchSessions: (moduleExports: any, moduleVersion?: string) => { + diag.debug(`Applying patch for mongodb@${moduleVersion}`); + if (isWrapped(moduleExports.acquire)) { + this._unwrap(moduleExports, 'acquire'); + } + this._wrap( + moduleExports.ServerSessionPool.prototype, + 'acquire', + this._getV4AcquireCommand() + ); + + if (isWrapped(moduleExports.release)) { + this._unwrap(moduleExports, 'release'); + } + this._wrap( + moduleExports.ServerSessionPool.prototype, + 'release', + this._getV4ReleaseCommand() + ); + return moduleExports; + }, + v4UnpatchSessions: (moduleExports?: T, moduleVersion?: string) => { + diag.debug(`Removing internal patch for mongodb@${moduleVersion}`); + if (moduleExports === undefined) return; + if (isWrapped(moduleExports.acquire)) { + this._unwrap(moduleExports, 'acquire'); + } + if (isWrapped(moduleExports.release)) { + this._unwrap(moduleExports, 'release'); + } + }, + }; + } + + private _getV4AcquireCommand() { + const instrumentation = this; + return (original: V4Session['acquire']) => { + return function patchAcquire(this: any) { + const nSessionsBeforeAcquire = this.sessions.length; + const session = original.call(this); + const nSessionsAfterAcquire = this.sessions.length; + + if (nSessionsBeforeAcquire === nSessionsAfterAcquire) { + //no session in the pool. a new session was created and used + instrumentation._connectionsUsage.add(1, { + state: 'used', + 'pool.name': instrumentation._poolName, + }); + } else if (nSessionsBeforeAcquire - 1 === nSessionsAfterAcquire) { + //a session was already in the pool. remove it from the pool and use it. + instrumentation._connectionsUsage.add(-1, { + state: 'idle', + 'pool.name': instrumentation._poolName, + }); + instrumentation._connectionsUsage.add(1, { + state: 'used', + 'pool.name': instrumentation._poolName, + }); + } + return session; + }; + }; + } + + private _getV4ReleaseCommand() { + const instrumentation = this; + return (original: V4Session['release']) => { + return function patchRelease(this: any, session: ServerSession) { + const cmdPromise = original.call(this, session); + + instrumentation._connectionsUsage.add(-1, { + state: 'used', + 'pool.name': instrumentation._poolName, + }); + instrumentation._connectionsUsage.add(1, { + state: 'idle', + 'pool.name': instrumentation._poolName, + }); + return cmdPromise; + }; + }; + } + + private _getV4ConnectPatches() { + return { + v4PatchConnect: (moduleExports: any, moduleVersion?: string) => { + diag.debug(`Applying patch for mongodb@${moduleVersion}`); + if (isWrapped(moduleExports.connect)) { + this._unwrap(moduleExports, 'connect'); + } + + this._wrap(moduleExports, 'connect', this._getV4ConnectCommand()); + return moduleExports; + }, + v4UnpatchConnect: (moduleExports?: T, moduleVersion?: string) => { + diag.debug(`Removing internal patch for mongodb@${moduleVersion}`); + if (moduleExports === undefined) return; + + this._unwrap(moduleExports, 'connect'); + }, + }; + } + + private _getV4ConnectCommand() { + const instrumentation = this; + + return (original: V4Connect['connect']) => { + return function patchedConnect( + this: unknown, + options: any, + callback: any + ) { + const patchedCallback = function (err: any, conn: any) { + if (err || !conn) { + callback(err, conn); + return; + } + instrumentation.setPoolName(options); + callback(err, conn); + }; + return original.call(this, options, patchedCallback); + }; + }; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars - private _getV4Patches() { + private _getV4ConnectionPatches() { return { - v4Patch: (moduleExports: any, moduleVersion?: string) => { + v4PatchConnection: (moduleExports: any, moduleVersion?: string) => { diag.debug(`Applying patch for mongodb@${moduleVersion}`); // patch insert operation if (isWrapped(moduleExports.Connection.prototype.command)) { @@ -164,7 +327,7 @@ export class MongoDBInstrumentation extends InstrumentationBase { ); return moduleExports; }, - v4Unpatch: (moduleExports?: any, moduleVersion?: string) => { + v4UnpatchConnection: (moduleExports?: any, moduleVersion?: string) => { if (moduleExports === undefined) return; diag.debug(`Removing internal patch for mongodb@${moduleVersion}`); this._unwrap(moduleExports.Connection.prototype, 'command'); @@ -275,7 +438,7 @@ export class MongoDBInstrumentation extends InstrumentationBase { const instrumentation = this; return (original: V4Connection['command']) => { return function patchedV4ServerCommand( - this: unknown, + this: any, ns: any, cmd: any, options: undefined | unknown, @@ -283,8 +446,9 @@ export class MongoDBInstrumentation extends InstrumentationBase { ) { const currentSpan = trace.getSpan(context.active()); const resultHandler = callback; + const commandType = Object.keys(cmd)[0]; + if ( - !currentSpan || typeof resultHandler !== 'function' || typeof cmd !== 'object' || cmd.ismaster || @@ -292,16 +456,38 @@ export class MongoDBInstrumentation extends InstrumentationBase { ) { return original.call(this, ns, cmd, options, callback); } - const commandType = Object.keys(cmd)[0]; - const span = instrumentation.tracer.startSpan( - `mongodb.${commandType}`, - { - kind: SpanKind.CLIENT, - } - ); - instrumentation._populateV4Attributes(span, this, ns, cmd, commandType); - const patchedCallback = instrumentation._patchEnd(span, resultHandler); - return original.call(this, ns, cmd, options, patchedCallback); + if (!currentSpan) { + const patchedCallback = instrumentation._patchEnd( + undefined, + resultHandler, + this.id, + commandType + ); + + return original.call(this, ns, cmd, options, patchedCallback); + } else { + const span = instrumentation.tracer.startSpan( + `mongodb.${commandType}`, + { + kind: SpanKind.CLIENT, + } + ); + instrumentation._populateV4Attributes( + span, + this, + ns, + cmd, + commandType + ); + const patchedCallback = instrumentation._patchEnd( + span, + resultHandler, + this.id, + commandType + ); + + return original.call(this, ns, cmd, options, patchedCallback); + } }; }; } @@ -575,6 +761,7 @@ export class MongoDBInstrumentation extends InstrumentationBase { [SemanticAttributes.DB_NAME]: dbName, [SemanticAttributes.DB_MONGODB_COLLECTION]: dbCollection, [SemanticAttributes.DB_OPERATION]: operation, + [SemanticAttributes.DB_CONNECTION_STRING]: `mongodb://${host}:${port}/${dbName}`, }); if (host && port) { @@ -640,28 +827,49 @@ export class MongoDBInstrumentation extends InstrumentationBase { * Ends a created span. * @param span The created span to end. * @param resultHandler A callback function. + * @param connectionId: The connection ID of the Command response. */ - private _patchEnd(span: Span, resultHandler: Function): Function { + private _patchEnd( + span: Span | undefined, + resultHandler: Function, + connectionId?: number, + commandType?: string + ): Function { // mongodb is using "tick" when calling a callback, this way the context // in final callback (resultHandler) is lost const activeContext = context.active(); const instrumentation = this; return function patchedEnd(this: {}, ...args: unknown[]) { const error = args[0]; - if (error instanceof Error) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message, - }); - } else { - const result = args[1] as CommandResult; - instrumentation._handleExecutionResult(span, result); + if (span) { + if (error instanceof Error) { + span?.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }); + } else { + const result = args[1] as CommandResult; + instrumentation._handleExecutionResult(span, result); + } + span.end(); } - span.end(); return context.with(activeContext, () => { + if (commandType === 'endSessions') { + instrumentation._connectionsUsage.add(-1, { + state: 'idle', + 'pool.name': instrumentation._poolName, + }); + } return resultHandler.apply(this, args); }); }; } + private setPoolName(options: any) { + const host = options.hostAddress?.host; + const port = options.hostAddress?.port; + const database = options.dbName; + const poolName = `mongodb://${host}:${port}/${database}`; + this._poolName = poolName; + } } diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/src/internal-types.ts b/plugins/node/opentelemetry-instrumentation-mongodb/src/internal-types.ts index 8e9ff0245de..03131aa12a4 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/src/internal-types.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/src/internal-types.ts @@ -64,6 +64,13 @@ export type MongoInternalCommand = { u?: Record; }; +export type ServerSession = { + id: any; + lastUse: number; + txnNumber: number; + isDirty: boolean; +}; + export type CursorState = { cmd: MongoInternalCommand } & Record< string, unknown @@ -176,3 +183,14 @@ export type V4Connection = { callback: any ): void; }; + +// https://github.com/mongodb/node-mongodb-native/blob/v4.2.2/src/cmap/connect.ts +export type V4Connect = { + connect: (options: any, callback: any) => void; +}; + +// https://github.com/mongodb/node-mongodb-native/blob/v4.2.2/src/sessions.ts +export type V4Session = { + acquire: () => ServerSession; + release: (session: ServerSession) => void; +}; diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts b/plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts index 9c75c45d146..e0389de007f 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts @@ -61,3 +61,22 @@ export type CommandResult = { connection?: unknown; message?: unknown; }; + +export enum MongodbCommandType { + CREATE_INDEXES = 'createIndexes', + FIND_AND_MODIFY = 'findAndModify', + IS_MASTER = 'isMaster', + COUNT = 'count', + UNKNOWN = 'unknown', +} + +// https://github.com/mongodb/node-mongodb-native/blob/v4.2.2/src/cmap/connection.ts +export type V4Connection = { + id: number | ''; + command( + ns: any, + cmd: Document, + options: undefined | unknown, + callback: any + ): void; +}; diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v3.test.ts b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v3.test.ts index 5a0b39e7736..f3368d75fc4 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v3.test.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v3.test.ts @@ -83,7 +83,7 @@ describe('MongoDBInstrumentation', () => { } // Non traced insertion of basic data to perform tests const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; - collection.insertMany(insertData, (err, result) => { + collection.insertMany(insertData, (err: any, result: any) => { resetMemoryExporter(); done(); }); @@ -116,7 +116,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.insert', SpanKind.CLIENT, - 'insert' + 'insert', + undefined ); done(); }) @@ -137,7 +138,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.update', SpanKind.CLIENT, - 'update' + 'update', + undefined ); done(); }) @@ -158,7 +160,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.remove', SpanKind.CLIENT, - 'remove' + 'remove', + undefined ); done(); }) @@ -183,7 +186,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.find', SpanKind.CLIENT, - 'find' + 'find', + undefined ); done(); }) @@ -211,7 +215,8 @@ describe('MongoDBInstrumentation', () => { ), 'mongodb.find', SpanKind.CLIENT, - 'find' + 'find', + undefined ); // assert that we correctly got the first as a find assertSpans( @@ -220,7 +225,8 @@ describe('MongoDBInstrumentation', () => { ), 'mongodb.getMore', SpanKind.CLIENT, - 'getMore' + 'getMore', + undefined ); done(); }) @@ -245,7 +251,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.createIndexes', SpanKind.CLIENT, - 'createIndexes' + 'createIndexes', + undefined ); done(); }) @@ -281,6 +288,7 @@ describe('MongoDBInstrumentation', () => { operationName, SpanKind.CLIENT, 'insert', + undefined, false, false ); @@ -326,6 +334,7 @@ describe('MongoDBInstrumentation', () => { operationName, SpanKind.CLIENT, 'insert', + undefined, false, true ); @@ -361,7 +370,13 @@ describe('MongoDBInstrumentation', () => { .then(() => { span.end(); const spans = getTestSpans(); - assertSpans(spans, 'mongodb.insert', SpanKind.CLIENT, 'insert'); + assertSpans( + spans, + 'mongodb.insert', + SpanKind.CLIENT, + 'insert', + undefined + ); done(); }) .catch(err => { @@ -458,7 +473,13 @@ describe('MongoDBInstrumentation', () => { .then(() => { span.end(); const spans = getTestSpans(); - assertSpans(spans, 'mongodb.find', SpanKind.CLIENT, 'find'); + assertSpans( + spans, + 'mongodb.find', + SpanKind.CLIENT, + 'find', + undefined + ); done(); }) .catch(err => { @@ -480,7 +501,13 @@ describe('MongoDBInstrumentation', () => { span.end(); const spans = getTestSpans(); const mainSpan = spans[spans.length - 1]; - assertSpans(spans, 'mongodb.insert', SpanKind.CLIENT, 'insert'); + assertSpans( + spans, + 'mongodb.insert', + SpanKind.CLIENT, + 'insert', + undefined + ); resetMemoryExporter(); collection @@ -490,7 +517,13 @@ describe('MongoDBInstrumentation', () => { const spans2 = getTestSpans(); spans2.push(mainSpan); - assertSpans(spans2, 'mongodb.find', SpanKind.CLIENT, 'find'); + assertSpans( + spans2, + 'mongodb.find', + SpanKind.CLIENT, + 'find', + undefined + ); assert.strictEqual( mainSpan.spanContext().spanId, spans2[0].parentSpanId diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.metrics.test.ts b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.metrics.test.ts new file mode 100644 index 00000000000..b6fb8c36d14 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.metrics.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright The 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. + */ + +// for testing locally "npm run docker:start" + +import { MongoDBInstrumentation } from '../src'; + +// TODO: use test-utils after the new package has released. +import { + AggregationTemporality, + DataPointType, + InMemoryMetricExporter, + MeterProvider, + PeriodicExportingMetricReader, + ResourceMetrics, +} from '@opentelemetry/sdk-metrics'; + +import * as mongodb from 'mongodb'; + +const otelTestingMeterProvider = new MeterProvider(); +const inMemoryMetricsExporter = new InMemoryMetricExporter( + AggregationTemporality.CUMULATIVE +); +const metricReader = new PeriodicExportingMetricReader({ + exporter: inMemoryMetricsExporter, + exportIntervalMillis: 100, + exportTimeoutMillis: 100, +}); + +otelTestingMeterProvider.addMetricReader(metricReader); + +import { registerInstrumentationTesting } from '@opentelemetry/contrib-test-utils'; +const instrumentation = registerInstrumentationTesting( + new MongoDBInstrumentation() +); + +instrumentation.setMeterProvider(otelTestingMeterProvider); + +import { accessCollection, DEFAULT_MONGO_HOST } from './utils'; + +import * as assert from 'assert'; + +async function waitForNumberOfExports( + exporter: InMemoryMetricExporter, + numberOfExports: number +): Promise { + if (numberOfExports <= 0) { + throw new Error('numberOfExports must be greater than or equal to 0'); + } + + let totalExports = 0; + while (totalExports < numberOfExports) { + await new Promise(resolve => setTimeout(resolve, 20)); + const exportedMetrics = exporter.getMetrics(); + totalExports = exportedMetrics.length; + } + + return exporter.getMetrics(); +} + +describe('MongoDBInstrumentation-Metrics', () => { + // For these tests, mongo must be running. Add RUN_MONGODB_TESTS to run + // these tests. + const RUN_MONGODB_TESTS = process.env.RUN_MONGODB_TESTS as string; + let shouldTest = true; + if (!RUN_MONGODB_TESTS) { + console.log('Skipping test-mongodb. Run MongoDB to test'); + shouldTest = false; + } + + const HOST = process.env.MONGODB_HOST || DEFAULT_MONGO_HOST; + const PORT = process.env.MONGODB_PORT || '27017'; + const DB_NAME = process.env.MONGODB_DB || 'opentelemetry-tests-metrics'; + const COLLECTION_NAME = 'test-metrics'; + const URL = `mongodb://${HOST}:${PORT}/${DB_NAME}`; + + let client: mongodb.MongoClient; + let collection: mongodb.Collection; + beforeEach(function mongoBeforeEach(done) { + // Skipping all tests in beforeEach() is a workaround. Mocha does not work + // properly when skipping tests in before() on nested describe() calls. + // https://github.com/mochajs/mocha/issues/2819 + if (!shouldTest) { + this.skip(); + } + inMemoryMetricsExporter.reset(); + done(); + }); + + it('Should add connection usage metrics', async () => { + const result = await accessCollection(URL, DB_NAME, COLLECTION_NAME); + client = result.client; + collection = result.collection; + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + await collection.insertMany(insertData); + await collection.deleteMany({}); + let exportedMetrics = await waitForNumberOfExports( + inMemoryMetricsExporter, + 1 + ); + + assert.strictEqual(exportedMetrics.length, 1); + let metrics = exportedMetrics[0].scopeMetrics[0].metrics; + assert.strictEqual(metrics.length, 1); + assert.strictEqual(metrics[0].dataPointType, DataPointType.SUM); + + assert.strictEqual( + metrics[0].descriptor.description, + 'The number of connections that are currently in state described by the state attribute.' + ); + assert.strictEqual(metrics[0].descriptor.unit, '{connection}'); + assert.strictEqual( + metrics[0].descriptor.name, + 'db.client.connections.usage' + ); + assert.strictEqual(metrics[0].dataPoints.length, 2); + assert.strictEqual(metrics[0].dataPoints[0].value, 0); + assert.strictEqual(metrics[0].dataPoints[0].attributes['state'], 'used'); + assert.strictEqual( + metrics[0].dataPoints[0].attributes['pool.name'], + `mongodb://${HOST}:${PORT}/${DB_NAME}` + ); + + assert.strictEqual(metrics[0].dataPoints[1].value, 1); + assert.strictEqual(metrics[0].dataPoints[1].attributes['state'], 'idle'); + assert.strictEqual( + metrics[0].dataPoints[1].attributes['pool.name'], + `mongodb://${HOST}:${PORT}/${DB_NAME}` + ); + await client.close(); + + exportedMetrics = await waitForNumberOfExports(inMemoryMetricsExporter, 2); + assert.strictEqual(exportedMetrics.length, 2); + metrics = exportedMetrics[1].scopeMetrics[0].metrics; + assert.strictEqual(metrics.length, 1); + assert.strictEqual(metrics[0].dataPointType, DataPointType.SUM); + + assert.strictEqual( + metrics[0].descriptor.description, + 'The number of connections that are currently in state described by the state attribute.' + ); + assert.strictEqual(metrics[0].dataPoints.length, 2); + assert.strictEqual(metrics[0].dataPoints[0].value, 0); + assert.strictEqual(metrics[0].dataPoints[0].attributes['state'], 'used'); + assert.strictEqual( + metrics[0].dataPoints[0].attributes['pool.name'], + `mongodb://${HOST}:${PORT}/${DB_NAME}` + ); + assert.strictEqual(metrics[0].dataPoints[1].value, 0); + assert.strictEqual(metrics[0].dataPoints[1].attributes['state'], 'idle'); + assert.strictEqual( + metrics[0].dataPoints[1].attributes['pool.name'], + `mongodb://${HOST}:${PORT}/${DB_NAME}` + ); + }); +}); diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.test.ts b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.test.ts index 3101a947d25..10025b8a74c 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.test.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.test.ts @@ -34,7 +34,7 @@ import * as mongodb from 'mongodb'; import { assertSpans, accessCollection, DEFAULT_MONGO_HOST } from './utils'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -describe('MongoDBInstrumentation', () => { +describe('MongoDBInstrumentation-Tracing', () => { function create(config: MongoDBInstrumentationConfig = {}) { instrumentation.setConfig(config); } @@ -50,14 +50,14 @@ describe('MongoDBInstrumentation', () => { const URL = `mongodb://${process.env.MONGODB_HOST || DEFAULT_MONGO_HOST}:${ process.env.MONGODB_PORT || '27017' }`; - const DB_NAME = process.env.MONGODB_DB || 'opentelemetry-tests'; - const COLLECTION_NAME = 'test'; + const DB_NAME = process.env.MONGODB_DB || 'opentelemetry-tests-traces'; + const COLLECTION_NAME = 'test-traces'; + const conn_string = `${URL}/${DB_NAME}`; let client: mongodb.MongoClient; let collection: mongodb.Collection; before(done => { - shouldTest = true; accessCollection(URL, DB_NAME, COLLECTION_NAME) .then(result => { client = result.client; @@ -82,7 +82,7 @@ describe('MongoDBInstrumentation', () => { } // Non traced insertion of basic data to perform tests const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; - collection.insertMany(insertData, (err, result) => { + collection.insertMany(insertData, (err: any, result: any) => { resetMemoryExporter(); done(); }); @@ -115,7 +115,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.insert', SpanKind.CLIENT, - 'insert' + 'insert', + conn_string ); done(); }) @@ -136,7 +137,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.update', SpanKind.CLIENT, - 'update' + 'update', + conn_string ); done(); }) @@ -157,7 +159,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.delete', SpanKind.CLIENT, - 'delete' + 'delete', + conn_string ); done(); }) @@ -182,7 +185,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.find', SpanKind.CLIENT, - 'find' + 'find', + conn_string ); done(); }) @@ -210,7 +214,8 @@ describe('MongoDBInstrumentation', () => { ), 'mongodb.find', SpanKind.CLIENT, - 'find' + 'find', + conn_string ); // assert that we correctly got the first as a find assertSpans( @@ -219,7 +224,8 @@ describe('MongoDBInstrumentation', () => { ), 'mongodb.getMore', SpanKind.CLIENT, - 'getMore' + 'getMore', + conn_string ); done(); }) @@ -244,7 +250,8 @@ describe('MongoDBInstrumentation', () => { getTestSpans(), 'mongodb.createIndexes', SpanKind.CLIENT, - 'createIndexes' + 'createIndexes', + conn_string ); done(); }) @@ -280,6 +287,7 @@ describe('MongoDBInstrumentation', () => { operationName, SpanKind.CLIENT, 'insert', + conn_string, false, false ); @@ -325,6 +333,7 @@ describe('MongoDBInstrumentation', () => { operationName, SpanKind.CLIENT, 'insert', + conn_string, false, true ); @@ -360,7 +369,13 @@ describe('MongoDBInstrumentation', () => { .then(() => { span.end(); const spans = getTestSpans(); - assertSpans(spans, 'mongodb.insert', SpanKind.CLIENT, 'insert'); + assertSpans( + spans, + 'mongodb.insert', + SpanKind.CLIENT, + 'insert', + conn_string + ); done(); }) .catch(err => { @@ -454,7 +469,13 @@ describe('MongoDBInstrumentation', () => { .then(() => { span.end(); const spans = getTestSpans(); - assertSpans(spans, 'mongodb.find', SpanKind.CLIENT, 'find'); + assertSpans( + spans, + 'mongodb.find', + SpanKind.CLIENT, + 'find', + conn_string + ); done(); }) .catch(err => { @@ -476,7 +497,13 @@ describe('MongoDBInstrumentation', () => { span.end(); const spans = getTestSpans(); const mainSpan = spans[spans.length - 1]; - assertSpans(spans, 'mongodb.insert', SpanKind.CLIENT, 'insert'); + assertSpans( + spans, + 'mongodb.insert', + SpanKind.CLIENT, + 'insert', + conn_string + ); resetMemoryExporter(); collection @@ -485,7 +512,13 @@ describe('MongoDBInstrumentation', () => { .then(() => { const spans2 = getTestSpans(); spans2.push(mainSpan); - assertSpans(spans2, 'mongodb.find', SpanKind.CLIENT, 'find'); + assertSpans( + spans2, + 'mongodb.find', + SpanKind.CLIENT, + 'find', + conn_string + ); assert.strictEqual( mainSpan.spanContext().spanId, spans2[0].parentSpanId diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/test/utils.ts b/plugins/node/opentelemetry-instrumentation-mongodb/test/utils.ts index 3da0a1833cb..02b26705f63 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/test/utils.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/test/utils.ts @@ -41,7 +41,9 @@ export function accessCollection( options: mongodb.MongoClientOptions = {} ): Promise { return new Promise((resolve, reject) => { - mongodb.MongoClient.connect(url, { serverSelectionTimeoutMS: 1000 }) + mongodb.MongoClient.connect(url, { + serverSelectionTimeoutMS: 1000, + }) .then(client => { const db = client.db(dbName); const collection = db.collection(collectionName); @@ -66,6 +68,7 @@ export function assertSpans( expectedName: string, expectedKind: SpanKind, expectedOperation: string, + expectedConnString: string | undefined, log = false, isEnhancedDatabaseReportingEnabled = false ) { @@ -93,6 +96,12 @@ export function assertSpans( process.env.MONGODB_HOST || DEFAULT_MONGO_HOST ); assert.strictEqual(mongoSpan.status.code, SpanStatusCode.UNSET); + if (expectedConnString) { + assert.strictEqual( + mongoSpan.attributes[SemanticAttributes.DB_CONNECTION_STRING], + expectedConnString + ); + } if (isEnhancedDatabaseReportingEnabled) { const dbStatement = JSON.parse(