From d6dad284c4add5eb0bb4b717c951816574214dbd Mon Sep 17 00:00:00 2001 From: Abhilash Date: Mon, 8 Dec 2025 12:53:32 +0530 Subject: [PATCH] feat(instrumentation-mongodb): added support for mongodb v7 --- package-lock.json | 91 ++- packages/instrumentation-mongodb/.tav.yml | 5 + packages/instrumentation-mongodb/README.md | 2 +- packages/instrumentation-mongodb/package.json | 11 +- .../src/instrumentation.ts | 8 +- ...ts => mongodb-v4-v5-v6-v7.metrics.test.ts} | 9 + .../test/mongodb-v7.test.ts | 764 ++++++++++++++++++ 7 files changed, 879 insertions(+), 11 deletions(-) rename packages/instrumentation-mongodb/test/{mongodb-v4-v5-v6.metrics.test.ts => mongodb-v4-v5-v6-v7.metrics.test.ts} (95%) create mode 100644 packages/instrumentation-mongodb/test/mongodb-v7.test.ts diff --git a/package-lock.json b/package-lock.json index 57d50cffb4..64d535f747 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13028,6 +13028,8 @@ "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/webidl-conversions": "*" } @@ -15434,6 +15436,8 @@ "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=16.20.1" } @@ -27018,6 +27022,8 @@ "integrity": "sha512-H3GtYujOJdeKIMLKBT9PwlDhGrQfplABNF1G904w6r5ZXKWyv77aB0X9B+rhmaAwjtllHzaEkvi9mkGVZxs2Bw==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.4", @@ -27065,6 +27071,8 @@ "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" @@ -38419,7 +38427,7 @@ "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@types/bson": "4.0.5", - "mongodb": "6.19.0" + "mongodb": "7.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -38428,6 +38436,87 @@ "@opentelemetry/api": "^1.3.0" } }, + "packages/instrumentation-mongodb/node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "packages/instrumentation-mongodb/node_modules/bson": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "packages/instrumentation-mongodb/node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "packages/instrumentation-mongodb/node_modules/mongodb-connection-string-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, "packages/instrumentation-mongoose": { "name": "@opentelemetry/instrumentation-mongoose", "version": "0.55.0", diff --git a/packages/instrumentation-mongodb/.tav.yml b/packages/instrumentation-mongodb/.tav.yml index 681ef1dcb6..14a0b510ba 100644 --- a/packages/instrumentation-mongodb/.tav.yml +++ b/packages/instrumentation-mongodb/.tav.yml @@ -22,3 +22,8 @@ mongodb: mode: max-7 node: '>=15.0.0' commands: npm run test-v5-v6 + - versions: + include: ">=7 <8" + mode: max-7 + node: '>=20.19.0' + commands: npm run test-v7 diff --git a/packages/instrumentation-mongodb/README.md b/packages/instrumentation-mongodb/README.md index 2aba6a5bd6..802dce11f0 100644 --- a/packages/instrumentation-mongodb/README.md +++ b/packages/instrumentation-mongodb/README.md @@ -17,7 +17,7 @@ npm install --save @opentelemetry/instrumentation-mongodb ### Supported Versions -- [`mongodb`](https://www.npmjs.com/package/mongodb) version `>=3.3.0 <7` +- [`mongodb`](https://www.npmjs.com/package/mongodb) version `>=3.3.0 <8` ## Usage diff --git a/packages/instrumentation-mongodb/package.json b/packages/instrumentation-mongodb/package.json index ccfbdc73b1..fe1cc22a51 100644 --- a/packages/instrumentation-mongodb/package.json +++ b/packages/instrumentation-mongodb/package.json @@ -16,12 +16,13 @@ "compile:with-dependencies": "nx run-many -t compile -p @opentelemetry/instrumentation-mongodb", "compile": "tsc -p .", "prepublishOnly": "npm run compile", - "tdd": "npm run test-v5-v6-run -- --watch-extensions ts --watch", - "test": "npm run test-v5-v6 && npm run test-unit", + "tdd": "npm run test-v7 -- --watch-extensions ts --watch", + "test": "npm run test-v7 && npm run test-unit", "test-unit": "nyc --no-clean mocha --require '@opentelemetry/contrib-test-utils' 'test/**/unit/*.test.ts'", "test-v3": "nyc --no-clean mocha --require '@opentelemetry/contrib-test-utils' 'test/**/mongodb-v3.test.ts'", - "test-v4": "nyc --no-clean mocha --require '@opentelemetry/contrib-test-utils' 'test/mongodb-v4-v5-v6.metrics.test.ts' 'test/**/mongodb-v4.test.ts'", - "test-v5-v6": "nyc --no-clean mocha --require '@opentelemetry/contrib-test-utils' 'test/mongodb-v4-v5-v6.metrics.test.ts' 'test/**/mongodb-v5-v6.test.ts'", + "test-v4": "nyc --no-clean mocha --require '@opentelemetry/contrib-test-utils' 'test/mongodb-v4-v5-v6-v7.metrics.test.ts' 'test/**/mongodb-v4.test.ts'", + "test-v5-v6": "nyc --no-clean mocha --require '@opentelemetry/contrib-test-utils' 'test/mongodb-v4-v5-v6-v7.metrics.test.ts' 'test/**/mongodb-v5-v6.test.ts'", + "test-v7": "nyc --no-clean mocha --require '@opentelemetry/contrib-test-utils' 'test/mongodb-v4-v5-v6-v7.metrics.test.ts' 'test/**/mongodb-v7.test.ts'", "test:with-services-env": "cross-env NODE_OPTIONS='-r dotenv/config' DOTENV_CONFIG_PATH=../../test/test-services.env npm test", "test-all-versions": "tav", "test-all-versions:with-services-env": "cross-env NODE_OPTIONS='-r dotenv/config' DOTENV_CONFIG_PATH=../../test/test-services.env npm run test-all-versions", @@ -61,7 +62,7 @@ "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@types/bson": "4.0.5", - "mongodb": "6.19.0" + "mongodb": "7.0.0" }, "dependencies": { "@opentelemetry/instrumentation": "^0.208.0" diff --git a/packages/instrumentation-mongodb/src/instrumentation.ts b/packages/instrumentation-mongodb/src/instrumentation.ts index 13390c0b17..1f703bed25 100644 --- a/packages/instrumentation-mongodb/src/instrumentation.ts +++ b/packages/instrumentation-mongodb/src/instrumentation.ts @@ -118,7 +118,7 @@ export class MongoDBInstrumentation extends InstrumentationBase=4.0.0 <7'], + ['>=4.0.0 <8'], undefined, undefined, [ @@ -130,7 +130,7 @@ export class MongoDBInstrumentation extends InstrumentationBase=6.4.0 <7'], + ['>=6.4.0 <8'], v4PatchConnectionPromise, v4UnpatchConnection ), @@ -142,13 +142,13 @@ export class MongoDBInstrumentation extends InstrumentationBase=4.0.0 <7'], + ['>=4.0.0 <8'], v4PatchConnect, v4UnpatchConnect ), new InstrumentationNodeModuleFile( 'mongodb/lib/sessions.js', - ['>=4.0.0 <7'], + ['>=4.0.0 <8'], v4PatchSessions, v4UnpatchSessions ), diff --git a/packages/instrumentation-mongodb/test/mongodb-v4-v5-v6.metrics.test.ts b/packages/instrumentation-mongodb/test/mongodb-v4-v5-v6-v7.metrics.test.ts similarity index 95% rename from packages/instrumentation-mongodb/test/mongodb-v4-v5-v6.metrics.test.ts rename to packages/instrumentation-mongodb/test/mongodb-v4-v5-v6-v7.metrics.test.ts index 47e925ac14..0428d058fb 100644 --- a/packages/instrumentation-mongodb/test/mongodb-v4-v5-v6.metrics.test.ts +++ b/packages/instrumentation-mongodb/test/mongodb-v4-v5-v6-v7.metrics.test.ts @@ -48,6 +48,7 @@ const instrumentation = registerInstrumentationTesting( import { accessCollection, DEFAULT_MONGO_HOST } from './utils'; import type { MongoClient, Collection } from 'mongodb'; import * as assert from 'assert'; +import * as semver from 'semver'; async function waitForNumberOfExports( exporter: InMemoryMetricExporter, @@ -77,6 +78,14 @@ describe('MongoDBInstrumentation-Metrics', () => { shouldTest = false; } + // MongoDB v7 requires Node.js >= 20.19.0 + if (!semver.satisfies(process.version, '>=20.19.0')) { + console.log( + `Skipping mongodb v7 tests. Node.js ${process.version} does not meet minimum requirement of >=20.19.0` + ); + 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'; diff --git a/packages/instrumentation-mongodb/test/mongodb-v7.test.ts b/packages/instrumentation-mongodb/test/mongodb-v7.test.ts new file mode 100644 index 0000000000..da6a26029f --- /dev/null +++ b/packages/instrumentation-mongodb/test/mongodb-v7.test.ts @@ -0,0 +1,764 @@ +/* + * 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 type { MongoClient, Collection } from 'mongodb'; +import { assertSpans, accessCollection, DEFAULT_MONGO_HOST } from './utils'; +import { ATTR_DB_STATEMENT } from '../src/semconv'; + +describe('MongoDBInstrumentation-Tracing-v7', () => { + 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: MongoClient; + let collection: 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. ' + err.message); + 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); + }); + }); + }); + }); + + it('should create child spans for concurrent cursor operations', done => { + const queries = [{ a: 1 }, { a: 2 }, { a: 3 }]; + const tasks = queries.map((query, idx) => { + return new Promise((resolve, reject) => { + process.nextTick(() => { + const span = trace + .getTracer('default') + .startSpan(`findRootSpan ${idx}`); + context.with(trace.setSpan(context.active(), span), () => { + collection + .find(query) + .toArray() + .then(() => { + resolve(span.end()); + }) + .catch(reject); + }); + }); + }); + }); + + Promise.all(tasks) + .then(() => { + const spans = getTestSpans(); + const roots = spans.filter(s => s.name.startsWith('findRootSpan')); + + roots.forEach(root => { + const rootId = root.spanContext().spanId; + const children = spans.filter( + s => s.parentSpanContext?.spanId === rootId + ); + assert.strictEqual(children.length, 1); + }); + 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); + }); + }); + }); + + it('should create a child span for aggregation', done => { + const span = trace.getTracer('default').startSpan('indexRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .aggregate([ + { $match: { key: 'value' } }, + { $group: { _id: '$a', count: { $sum: 1 } } }, + ]) + .toArray() + .then(() => { + span.end(); + assertSpans( + getTestSpans(), + 'mongodb.aggregate', + SpanKind.CLIENT, + 'aggregate', + undefined + ); + 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[ATTR_DB_STATEMENT] as string + ); + assert.strictEqual(dbStatement[key], '?'); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + + it('should properly collect nested db statement (hide attribute values)', done => { + const span = trace.getTracer('default').startSpan('insertRootSpan'); + context.with(trace.setSpan(context.active(), span), () => { + collection + .aggregate([ + { $match: { key: 'value' } }, + { $group: { _id: '$a', count: { $sum: 1 } } }, + ]) + .toArray() + .then(() => { + span.end(); + const spans = getTestSpans(); + const operationName = 'mongodb.aggregate'; + assertSpans( + spans, + operationName, + SpanKind.CLIENT, + 'aggregate', + undefined, + false, + false + ); + const mongoSpan = spans.find(s => s.name === operationName); + const dbStatement = JSON.parse( + mongoSpan!.attributes[ATTR_DB_STATEMENT] as string + ); + assert.deepEqual(dbStatement, { + aggregate: '?', + pipeline: [ + { $match: { key: '?' } }, + { $group: { _id: '?', count: { $sum: '?' } } }, + ], + cursor: {}, + }); + 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[ATTR_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', () => { + describe('with a valid function', () => { + beforeEach(() => { + create({ + responseHook: (span: Span, result: any) => { + const { data } = result; + + // from 6.19.0 insert returns a MongoDBResponse class with a + // `toObject()` method instead of the plain { ok: 1, n: [Number] } response + let insertCount = data.n; + if (!insertCount && typeof data.toObject === 'function') { + insertCount = data.toObject().n; + } + + if (insertCount) { + span.setAttribute('mongodb_insert_count', insertCount); + } + + // from v6.8.0 the cursor property is not an object but an instance of + // `CursorResponse`. We need to use the `toObject` method to be able to inspect the data + if (data.cursor) { + const cursorObj = data.cursor.firstBatch + ? data.cursor + : data.cursor.toObject(); + span.setAttribute( + 'mongodb_first_result', + JSON.stringify(cursorObj.firstBatch[0]) + ); + } + }, + }); + }); + + 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( + insertSpan.attributes['mongodb_insert_count'], + 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['mongodb_first_result'] as string + ); + + if (results) { + assert.strictEqual( + hookAttributeValue?._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].parentSpanContext?.spanId + ); + done(); + }) + .catch(err => { + done(err); + }); + }) + .catch(err => { + done(err); + }); + }); + }); + }); + + describe('requireParentSpan', () => { + // Resetting the behavior to default to avoid flakes in other tests + beforeEach(() => { + instrumentation.setConfig(); + }); + + afterEach(() => { + instrumentation.setConfig(); + }); + + it('should not create spans without parent span when requireParentSpan is explicitly set to true', done => { + context.with(trace.deleteSpan(context.active()), () => { + collection + .insertOne({ a: 1 }) + .then(() => { + assert.strictEqual(getTestSpans().length, 0); + done(); + }) + .catch(err => { + done(err); + }); + }); + }); + + it('should create spans without parent span when requireParentSpan is false', done => { + instrumentation.setConfig({ + requireParentSpan: false, + }); + + context.with(trace.deleteSpan(context.active()), () => { + collection + .insertOne({ a: 1 }) + .then(() => { + assert.strictEqual(getTestSpans().length, 1); + done(); + }) + .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); + }); + }); + }); +});