diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/.tav.yml b/plugins/node/opentelemetry-instrumentation-mongodb/.tav.yml index c369d133f6..86a2bf927d 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/.tav.yml +++ b/plugins/node/opentelemetry-instrumentation-mongodb/.tav.yml @@ -1,9 +1,11 @@ mongodb: jobs: - versions: ">=3.3 <4" - commands: npm run test + commands: npm run test-v3 - versions: ">=4 <5" - commands: npm run test-new-versions + commands: npm run test-v4 + - versions: ">=5 <6" + commands: npm run test-v5 # Fix missing `contrib-test-utils` package pretest: npm run --prefix ../../../ lerna:link diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/package.json b/plugins/node/opentelemetry-instrumentation-mongodb/package.json index ef186f31ff..e2a54723be 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/package.json +++ b/plugins/node/opentelemetry-instrumentation-mongodb/package.json @@ -7,8 +7,10 @@ "repository": "open-telemetry/opentelemetry-js-contrib", "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": "npm run test-v3", + "test-v3": "nyc ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/**/mongodb-v3.test.ts'", + "test-v4": "nyc ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/mongodb-v4-v5.metrics.test.ts' 'test/**/mongodb-v4.test.ts'", + "test-v5": "nyc ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/mongodb-v4-v5.metrics.test.ts' 'test/**/mongodb-v5.test.ts'", "test-all-versions": "tav", "tdd": "npm run test -- --watch-extensions ts --watch", "clean": "rimraf build/*", @@ -55,12 +57,12 @@ "@opentelemetry/context-async-hooks": "^1.8.0", "@opentelemetry/sdk-trace-base": "^1.8.0", "@opentelemetry/sdk-trace-node": "^1.8.0", + "@types/bson": "4.0.5", "@types/mocha": "7.0.2", + "@types/mongodb": "3.6.20", "@types/node": "18.11.7", "mocha": "7.2.0", "mongodb": "3.6.11", - "@types/mongodb": "3.6.20", - "@types/bson": "4.0.5", "nyc": "15.1.0", "rimraf": "5.0.0", "test-all-versions": "5.0.1", diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts index f1f66f8f49..587c0fe185 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts @@ -95,25 +95,25 @@ export class MongoDBInstrumentation extends InstrumentationBase { ), new InstrumentationNodeModuleDefinition( 'mongodb', - ['4.*'], + ['4.*', '5.*'], undefined, undefined, [ new InstrumentationNodeModuleFile( 'mongodb/lib/cmap/connection.js', - ['4.*'], + ['4.*', '5.*'], v4PatchConnection, v4UnpatchConnection ), new InstrumentationNodeModuleFile( 'mongodb/lib/cmap/connect.js', - ['4.*'], + ['4.*', '5.*'], v4PatchConnect, v4UnpatchConnect ), new InstrumentationNodeModuleFile( 'mongodb/lib/sessions.js', - ['4.*'], + ['4.*', '5.*'], v4PatchSessions, v4UnpatchSessions ), 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 f3368d75fc..a8f0444839 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v3.test.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v3.test.ts @@ -35,7 +35,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-v3', () => { function create(config: MongoDBInstrumentationConfig = {}) { instrumentation.setConfig(config); } diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.metrics.test.ts b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4-v5.metrics.test.ts similarity index 76% rename from plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.metrics.test.ts rename to plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4-v5.metrics.test.ts index b6fb8c36d1..189ff458d6 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.metrics.test.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4-v5.metrics.test.ts @@ -28,8 +28,6 @@ import { ResourceMetrics, } from '@opentelemetry/sdk-metrics'; -import * as mongodb from 'mongodb'; - const otelTestingMeterProvider = new MeterProvider(); const inMemoryMetricsExporter = new InMemoryMetricExporter( AggregationTemporality.CUMULATIVE @@ -40,17 +38,13 @@ const metricReader = new PeriodicExportingMetricReader({ 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 mongodb from 'mongodb'; import * as assert from 'assert'; async function waitForNumberOfExports( @@ -86,9 +80,13 @@ describe('MongoDBInstrumentation-Metrics', () => { 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; + + before(() => { + otelTestingMeterProvider.addMetricReader(metricReader); + instrumentation?.setMeterProvider(otelTestingMeterProvider); + }); + 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. @@ -96,6 +94,7 @@ describe('MongoDBInstrumentation-Metrics', () => { if (!shouldTest) { this.skip(); } + inMemoryMetricsExporter.reset(); done(); }); @@ -103,17 +102,17 @@ describe('MongoDBInstrumentation-Metrics', () => { it('Should add connection usage metrics', async () => { const result = await accessCollection(URL, DB_NAME, COLLECTION_NAME); client = result.client; - collection = result.collection; + const collection = result.collection; const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; await collection.insertMany(insertData); await collection.deleteMany({}); - let exportedMetrics = await waitForNumberOfExports( + const exportedMetrics = await waitForNumberOfExports( inMemoryMetricsExporter, 1 ); assert.strictEqual(exportedMetrics.length, 1); - let metrics = exportedMetrics[0].scopeMetrics[0].metrics; + const metrics = exportedMetrics[0].scopeMetrics[0].metrics; assert.strictEqual(metrics.length, 1); assert.strictEqual(metrics[0].dataPointType, DataPointType.SUM); @@ -126,25 +125,34 @@ describe('MongoDBInstrumentation-Metrics', () => { 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'); + + // Checking dataPoints + const dataPoints = metrics[0].dataPoints; + assert.strictEqual(dataPoints.length, 2); + assert.strictEqual(dataPoints[0].value, 0); + assert.strictEqual(dataPoints[0].attributes['state'], 'used'); assert.strictEqual( - metrics[0].dataPoints[0].attributes['pool.name'], + 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(dataPoints[1].value, 1); + assert.strictEqual(dataPoints[1].attributes['state'], 'idle'); assert.strictEqual( - metrics[0].dataPoints[1].attributes['pool.name'], + dataPoints[1].attributes['pool.name'], `mongodb://${HOST}:${PORT}/${DB_NAME}` ); + }); + + it('Should add disconnection usage metrics', async () => { await client.close(); - exportedMetrics = await waitForNumberOfExports(inMemoryMetricsExporter, 2); + const exportedMetrics = await waitForNumberOfExports( + inMemoryMetricsExporter, + 2 + ); assert.strictEqual(exportedMetrics.length, 2); - metrics = exportedMetrics[1].scopeMetrics[0].metrics; + const metrics = exportedMetrics[1].scopeMetrics[0].metrics; assert.strictEqual(metrics.length, 1); assert.strictEqual(metrics[0].dataPointType, DataPointType.SUM); @@ -152,17 +160,20 @@ describe('MongoDBInstrumentation-Metrics', () => { 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'); + + // Checking dataPoints + const dataPoints = metrics[0].dataPoints; + assert.strictEqual(dataPoints.length, 2); + assert.strictEqual(dataPoints[0].value, 0); + assert.strictEqual(dataPoints[0].attributes['state'], 'used'); assert.strictEqual( - metrics[0].dataPoints[0].attributes['pool.name'], + 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(dataPoints[1].value, 0); + assert.strictEqual(dataPoints[1].attributes['state'], 'idle'); assert.strictEqual( - metrics[0].dataPoints[1].attributes['pool.name'], + 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 10025b8a74..18ba6bc9ef 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.test.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.test.ts @@ -18,8 +18,11 @@ import { context, trace, SpanKind, Span } from '@opentelemetry/api'; import * as assert from 'assert'; -import { MongoDBInstrumentation, MongoDBInstrumentationConfig } from '../src'; -import { MongoResponseHookInformation } from '../src'; +import { + MongoDBInstrumentation, + MongoDBInstrumentationConfig, + MongoResponseHookInformation, +} from '../src'; import { registerInstrumentationTesting, getTestSpans, @@ -34,7 +37,7 @@ import * as mongodb from 'mongodb'; import { assertSpans, accessCollection, DEFAULT_MONGO_HOST } from './utils'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -describe('MongoDBInstrumentation-Tracing', () => { +describe('MongoDBInstrumentation-Tracing-v4', () => { function create(config: MongoDBInstrumentationConfig = {}) { instrumentation.setConfig(config); } @@ -47,12 +50,11 @@ describe('MongoDBInstrumentation-Tracing', () => { shouldTest = false; } - const URL = `mongodb://${process.env.MONGODB_HOST || DEFAULT_MONGO_HOST}:${ - process.env.MONGODB_PORT || '27017' - }`; + 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-traces'; const COLLECTION_NAME = 'test-traces'; - const conn_string = `${URL}/${DB_NAME}`; + const URL = `mongodb://${HOST}:${PORT}/${DB_NAME}`; let client: mongodb.MongoClient; let collection: mongodb.Collection; @@ -95,9 +97,9 @@ describe('MongoDBInstrumentation-Tracing', () => { done(); }); - after(() => { + after(async () => { if (client) { - client.close(); + await client.close(); } }); @@ -116,7 +118,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.insert', SpanKind.CLIENT, 'insert', - conn_string + URL ); done(); }) @@ -138,7 +140,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.update', SpanKind.CLIENT, 'update', - conn_string + URL ); done(); }) @@ -160,7 +162,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.delete', SpanKind.CLIENT, 'delete', - conn_string + URL ); done(); }) @@ -186,7 +188,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.find', SpanKind.CLIENT, 'find', - conn_string + URL ); done(); }) @@ -215,7 +217,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.find', SpanKind.CLIENT, 'find', - conn_string + URL ); // assert that we correctly got the first as a find assertSpans( @@ -225,7 +227,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.getMore', SpanKind.CLIENT, 'getMore', - conn_string + URL ); done(); }) @@ -251,7 +253,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.createIndexes', SpanKind.CLIENT, 'createIndexes', - conn_string + URL ); done(); }) @@ -287,7 +289,7 @@ describe('MongoDBInstrumentation-Tracing', () => { operationName, SpanKind.CLIENT, 'insert', - conn_string, + URL, false, false ); @@ -333,7 +335,7 @@ describe('MongoDBInstrumentation-Tracing', () => { operationName, SpanKind.CLIENT, 'insert', - conn_string, + URL, false, true ); @@ -374,7 +376,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.insert', SpanKind.CLIENT, 'insert', - conn_string + URL ); done(); }) @@ -469,13 +471,7 @@ describe('MongoDBInstrumentation-Tracing', () => { .then(() => { span.end(); const spans = getTestSpans(); - assertSpans( - spans, - 'mongodb.find', - SpanKind.CLIENT, - 'find', - conn_string - ); + assertSpans(spans, 'mongodb.find', SpanKind.CLIENT, 'find', URL); done(); }) .catch(err => { @@ -502,7 +498,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.insert', SpanKind.CLIENT, 'insert', - conn_string + URL ); resetMemoryExporter(); @@ -517,7 +513,7 @@ describe('MongoDBInstrumentation-Tracing', () => { 'mongodb.find', SpanKind.CLIENT, 'find', - conn_string + URL ); assert.strictEqual( mainSpan.spanContext().spanId, diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v5.test.ts b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v5.test.ts new file mode 100644 index 0000000000..bd8271cb8d --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v5.test.ts @@ -0,0 +1,594 @@ +/* + * 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 { context, trace, SpanKind, Span } from '@opentelemetry/api'; +import * as assert from 'assert'; +import { + MongoDBInstrumentation, + MongoDBInstrumentationConfig, + MongoResponseHookInformation, +} from '../src'; +import { + getInstrumentation, + registerInstrumentationTesting, + getTestSpans, + resetMemoryExporter, +} from '@opentelemetry/contrib-test-utils'; + +// Get instrumentation (singleton) +let instrumentation: MongoDBInstrumentation; +{ + const instance: MongoDBInstrumentation | undefined = getInstrumentation(); + if (!instance) { + instrumentation = new MongoDBInstrumentation(); + registerInstrumentationTesting(instrumentation); + } else { + instrumentation = instance; + } +} + +import * as mongodb from 'mongodb'; +import { assertSpans, accessCollection, DEFAULT_MONGO_HOST } from './utils'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +describe('MongoDBInstrumentation-Tracing-v5', () => { + function create(config: MongoDBInstrumentationConfig = {}) { + instrumentation.setConfig(config); + } + // 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-traces'; + const COLLECTION_NAME = 'test-traces'; + const URL = `mongodb://${HOST}:${PORT}/${DB_NAME}`; + + let client: mongodb.MongoClient; + let collection: mongodb.Collection; + + before(done => { + 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(async function mongoBeforeEach() { + // 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(); + } + // Non traced insertion of basic data to perform tests + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + await collection.insertMany(insertData); + resetMemoryExporter(); + }); + + afterEach(async () => { + if (shouldTest) { + await collection.deleteMany({}); + } + }); + + after(async () => { + if (client) { + await 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 = trace.getTracer('default').startSpan('insertRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .insertMany(insertData) + .then(() => { + span.end(); + assertSpans( + getTestSpans(), + 'mongodb.insert', + SpanKind.CLIENT, + 'insert', + URL + ); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + + it('should create a child span for update', done => { + const span = trace.getTracer('default').startSpan('updateRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .updateOne({ a: 2 }, { $set: { b: 1 } }) + .then(() => { + span.end(); + assertSpans( + getTestSpans(), + 'mongodb.update', + SpanKind.CLIENT, + 'update', + URL + ); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + + it('should create a child span for remove', done => { + const span = trace.getTracer('default').startSpan('removeRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .deleteOne({ a: 3 }) + .then(() => { + span.end(); + assertSpans( + getTestSpans(), + 'mongodb.delete', + SpanKind.CLIENT, + 'delete', + URL + ); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + + /** Should intercept cursor */ + describe('Instrumenting cursor operations', () => { + it('should create a child span for find', done => { + const span = trace.getTracer('default').startSpan('findRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .find({ a: 1 }) + .toArray() + .then(() => { + span.end(); + assertSpans( + getTestSpans(), + 'mongodb.find', + SpanKind.CLIENT, + 'find', + URL + ); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + + it('should create a child span for cursor operations', done => { + const span = trace.getTracer('default').startSpan('findRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + const cursor = collection.find().batchSize(1); + cursor.next().then(firstElement => { + assert(firstElement !== null); + cursor + .next() + .then(secondElement => { + span.end(); + assert(secondElement !== null); + // assert that we correctly got the first as a find + assertSpans( + getTestSpans().filter( + span => !span.name.includes('mongodb.getMore') + ), + 'mongodb.find', + SpanKind.CLIENT, + 'find', + URL + ); + // assert that we correctly got the first as a find + assertSpans( + getTestSpans().filter( + span => !span.name.includes('mongodb.find') + ), + 'mongodb.getMore', + SpanKind.CLIENT, + 'getMore', + URL + ); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + }); + + /** Should intercept command */ + describe('Instrumenting command operations', () => { + it('should create a child span for create index', done => { + const span = trace.getTracer('default').startSpan('indexRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .createIndex({ a: 1 }) + .then(() => { + span.end(); + assertSpans( + getTestSpans(), + 'mongodb.createIndexes', + SpanKind.CLIENT, + 'createIndexes', + URL + ); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + + describe('when using enhanced database reporting without db statementSerializer', () => { + const key = 'key'; + const value = 'value'; + const object = { [key]: value }; + + beforeEach(() => { + create({ + enhancedDatabaseReporting: false, + }); + }); + + it('should properly collect db statement (hide attribute values)', done => { + const span = trace.getTracer('default').startSpan('insertRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .insertOne(object) + .then(() => { + span.end(); + const spans = getTestSpans(); + const operationName = 'mongodb.insert'; + assertSpans( + spans, + operationName, + SpanKind.CLIENT, + 'insert', + URL, + false, + false + ); + const mongoSpan = spans.find(s => s.name === operationName); + const dbStatement = JSON.parse( + mongoSpan!.attributes[SemanticAttributes.DB_STATEMENT] as string + ); + assert.strictEqual(dbStatement[key], '?'); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + + describe('when specifying a dbStatementSerializer configuration', () => { + const key = 'key'; + const value = 'value'; + const object = { [key]: value }; + + describe('with a valid function', () => { + beforeEach(() => { + create({ + dbStatementSerializer: (commandObj: Record) => { + return JSON.stringify(commandObj); + }, + }); + }); + + it('should properly collect db statement', done => { + const span = trace.getTracer('default').startSpan('insertRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .insertOne(object) + .then(() => { + span.end(); + const spans = getTestSpans(); + const operationName = 'mongodb.insert'; + assertSpans( + spans, + operationName, + SpanKind.CLIENT, + 'insert', + URL, + false, + true + ); + const mongoSpan = spans.find(s => s.name === operationName); + const dbStatement = JSON.parse( + mongoSpan!.attributes[SemanticAttributes.DB_STATEMENT] as string + ); + assert.strictEqual(dbStatement[key], value); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + + describe('with an invalid function', () => { + beforeEach(() => { + create({ + enhancedDatabaseReporting: true, + dbStatementSerializer: (_commandObj: Record) => { + throw new Error('something went wrong!'); + }, + }); + }); + + it('should not do any harm when throwing an exception', done => { + const span = trace.getTracer('default').startSpan('insertRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .insertOne(object) + .then(() => { + span.end(); + const spans = getTestSpans(); + assertSpans( + spans, + 'mongodb.insert', + SpanKind.CLIENT, + 'insert', + URL + ); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + }); + + describe('when specifying a responseHook configuration', () => { + const dataAttributeName = 'mongodb_data'; + describe('with a valid function', () => { + beforeEach(() => { + create({ + responseHook: (span: Span, result: MongoResponseHookInformation) => { + span.setAttribute(dataAttributeName, JSON.stringify(result.data)); + }, + }); + }); + + it('should attach response hook data to the resulting span for insert function', done => { + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + const span = trace.getTracer('default').startSpan('insertRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .insertMany(insertData) + .then(results => { + span.end(); + const spans = getTestSpans(); + const insertSpan = spans[0]; + assert.deepStrictEqual( + JSON.parse(insertSpan.attributes[dataAttributeName] as string) + .n, + results?.insertedCount + ); + + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + + it('should attach response hook data to the resulting span for find function', done => { + const span = trace.getTracer('default').startSpan('findRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .find({ a: 1 }) + .toArray() + .then(results => { + span.end(); + const spans = getTestSpans(); + const findSpan = spans[0]; + const hookAttributeValue = JSON.parse( + findSpan.attributes[dataAttributeName] as string + ); + + if (results) { + assert.strictEqual( + hookAttributeValue?.cursor?.firstBatch[0]._id, + results[0]._id.toString() + ); + } else { + throw new Error('Got an unexpected Results: ' + results); + } + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + + describe('with an invalid function', () => { + beforeEach(() => { + create({ + responseHook: (span: Span, result: MongoResponseHookInformation) => { + throw 'some error'; + }, + }); + }); + it('should not do any harm when throwing an exception', done => { + const span = trace.getTracer('default').startSpan('findRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .find({ a: 1 }) + .toArray() + .then(() => { + span.end(); + const spans = getTestSpans(); + assertSpans(spans, 'mongodb.find', SpanKind.CLIENT, 'find', URL); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + }); + + describe('Mixed operations with callback', () => { + it('should create a span for find after callback insert', done => { + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + const span = trace.getTracer('default').startSpan('insertRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .insertMany(insertData) + .then(() => { + span.end(); + const spans = getTestSpans(); + const mainSpan = spans[spans.length - 1]; + assertSpans( + spans, + 'mongodb.insert', + SpanKind.CLIENT, + 'insert', + URL + ); + resetMemoryExporter(); + + collection + .find({ a: 1 }) + .toArray() + .then(() => { + const spans2 = getTestSpans(); + spans2.push(mainSpan); + assertSpans( + spans2, + 'mongodb.find', + SpanKind.CLIENT, + 'find', + URL + ); + assert.strictEqual( + mainSpan.spanContext().spanId, + spans2[0].parentSpanId + ); + done(); + }) + .catch(err => { + done(err); + }); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + + /** Should intercept command */ + describe('Removing Instrumentation', () => { + it('should unpatch plugin', () => { + assert.doesNotThrow(() => { + instrumentation.disable(); + }); + }); + + it('should not create a child span for query', done => { + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + const span = trace.getTracer('default').startSpan('insertRootSpan'); + collection + .insertMany(insertData) + .then(() => { + span.end(); + assert.strictEqual(getTestSpans().length, 1); + done(); + }) + .catch(err => { + done(err); + }); + }); + + it('should not create a child span for cursor', done => { + const span = trace.getTracer('default').startSpan('findRootSpan'); + collection + .find({}) + .toArray() + .then(() => { + span.end(); + assert.strictEqual(getTestSpans().length, 1); + done(); + }) + .catch(err => { + assert.ifError(err); + done(err); + }); + }); + + it('should not create a child span for command', done => { + const span = trace.getTracer('default').startSpan('indexRootSpan'); + collection + .createIndex({ a: 1 }) + .then(() => { + span.end(); + assert.strictEqual(getTestSpans().length, 1); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); +});