diff --git a/package-lock.json b/package-lock.json index b9ae6a95de..b187276d0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27452,9 +27452,9 @@ } }, "node_modules/mongodb": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.17.1.tgz", - "integrity": "sha512-MBuyYiPUPRTqfH2dV0ya4dcr2E5N52ocBuZ8Sgg/M030nGF78v855B3Z27mZJnp8PxjnUquEnAtjOsphgMZOlQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.17.2.tgz", + "integrity": "sha512-mLV7SEiov2LHleRJPMPrK2PMyhXFZt2UQLC4VD4pnth3jMjYKHhtqfwwkkvS/NXuo/Fp3vbhaNcXrIDaLRb9Tg==", "dev": true, "dependencies": { "bson": "^4.7.2", @@ -27480,14 +27480,14 @@ } }, "node_modules/mongoose": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.12.3.tgz", - "integrity": "sha512-MNJymaaXali7w7rHBxVUoQ3HzHHMk/7I/+yeeoSa4rUzdjZwIWQznBNvVgc0A8ghuJwsuIkb5LyLV6gSjGjWyQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.13.0.tgz", + "integrity": "sha512-mieZBTtRIqA2xCGgl9Hlcr6fXU+AKNSOdeKfMYrb/IgdL3M/bDO4kYftsItIy86XyAoT5xV28alfCbMocFG8oA==", "dev": true, "dependencies": { "bson": "^4.7.2", "kareem": "2.5.1", - "mongodb": "4.17.1", + "mongodb": "4.17.2", "mpath": "0.9.0", "mquery": "4.0.3", "ms": "2.1.3", @@ -37929,7 +37929,7 @@ "@types/node": "18.6.5", "expect": "29.2.0", "mocha": "7.2.0", - "mongoose": "6.12.3", + "mongoose": "6.13.0", "nyc": "15.1.0", "rimraf": "5.0.5", "test-all-versions": "6.1.0", @@ -52816,7 +52816,7 @@ "@types/node": "18.6.5", "expect": "29.2.0", "mocha": "7.2.0", - "mongoose": "6.12.3", + "mongoose": "6.13.0", "nyc": "15.1.0", "rimraf": "5.0.5", "test-all-versions": "6.1.0", @@ -68687,9 +68687,9 @@ "optional": true }, "mongodb": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.17.1.tgz", - "integrity": "sha512-MBuyYiPUPRTqfH2dV0ya4dcr2E5N52ocBuZ8Sgg/M030nGF78v855B3Z27mZJnp8PxjnUquEnAtjOsphgMZOlQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.17.2.tgz", + "integrity": "sha512-mLV7SEiov2LHleRJPMPrK2PMyhXFZt2UQLC4VD4pnth3jMjYKHhtqfwwkkvS/NXuo/Fp3vbhaNcXrIDaLRb9Tg==", "dev": true, "requires": { "@aws-sdk/credential-providers": "^3.186.0", @@ -68710,14 +68710,14 @@ } }, "mongoose": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.12.3.tgz", - "integrity": "sha512-MNJymaaXali7w7rHBxVUoQ3HzHHMk/7I/+yeeoSa4rUzdjZwIWQznBNvVgc0A8ghuJwsuIkb5LyLV6gSjGjWyQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.13.0.tgz", + "integrity": "sha512-mieZBTtRIqA2xCGgl9Hlcr6fXU+AKNSOdeKfMYrb/IgdL3M/bDO4kYftsItIy86XyAoT5xV28alfCbMocFG8oA==", "dev": true, "requires": { "bson": "^4.7.2", "kareem": "2.5.1", - "mongodb": "4.17.1", + "mongodb": "4.17.2", "mpath": "0.9.0", "mquery": "4.0.3", "ms": "2.1.3", diff --git a/plugins/node/instrumentation-mongoose/.tav.yml b/plugins/node/instrumentation-mongoose/.tav.yml index 9824934548..ba89e40faf 100644 --- a/plugins/node/instrumentation-mongoose/.tav.yml +++ b/plugins/node/instrumentation-mongoose/.tav.yml @@ -1,5 +1,15 @@ -'mongoose': - # Test all the latest minor versions in the range ">=5.9.7 <7". - versions: "5.9.7 || 5.9.29 || 5.10.19 || 5.11.20 || 5.12.15 || 5.13.21 || 6.0.15 || 6.1.10 || 6.2.11 || 6.3.9 || 6.4.7 || 6.5.5 || 6.6.7 || 6.7.5 || 6.8.4 || 6.9.3 || 6.10.5 || 6.11.6 || ^6.12.3" - commands: - - npm run test +mongoose: + - versions: + include: ">=5.9.7 <7" + mode: latest-minors + commands: npm run test-v5-v6 + - versions: + include: ">=7 <8" + mode: latest-minors + node: '>=14.20.1' + commands: npm run test-v7-v8 + - versions: + include: ">=8 <9" + mode: latest-minors + node: '>=16.20.1' + commands: npm run test-v7-v8 diff --git a/plugins/node/instrumentation-mongoose/README.md b/plugins/node/instrumentation-mongoose/README.md index c5500b14c2..31a19b4e7d 100644 --- a/plugins/node/instrumentation-mongoose/README.md +++ b/plugins/node/instrumentation-mongoose/README.md @@ -17,7 +17,7 @@ npm install --save @opentelemetry/instrumentation-mongoose ## Supported Versions -- [`mongoose`](https://www.npmjs.com/package/mongoose) versions `>=5.9.7 <7` +- [`mongoose`](https://www.npmjs.com/package/mongoose) versions `>=5.9.7 <9` ## Usage diff --git a/plugins/node/instrumentation-mongoose/package.json b/plugins/node/instrumentation-mongoose/package.json index 8f5bcc772e..52b06e6fe9 100644 --- a/plugins/node/instrumentation-mongoose/package.json +++ b/plugins/node/instrumentation-mongoose/package.json @@ -7,7 +7,9 @@ "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": "ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/**/*.test.ts'", + "test": "npm run test-v5-v6", + "test-v5-v6": "nyc ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/mongoose-common.test.ts' 'test/**/mongoose-v5-v6.test.ts'", + "test-v7-v8": "nyc ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/mongoose-common.test.ts' 'test/**/mongoose-v7-v8.test.ts'", "test-all-versions": "tav", "tdd": "npm run test -- --watch-extensions ts --watch", "clean": "rimraf build/*", @@ -53,7 +55,7 @@ "@types/node": "18.6.5", "expect": "29.2.0", "mocha": "7.2.0", - "mongoose": "6.12.3", + "mongoose": "6.13.0", "nyc": "15.1.0", "rimraf": "5.0.5", "test-all-versions": "6.1.0", diff --git a/plugins/node/instrumentation-mongoose/src/mongoose.ts b/plugins/node/instrumentation-mongoose/src/mongoose.ts index 18f6a41c87..e73dee78c2 100644 --- a/plugins/node/instrumentation-mongoose/src/mongoose.ts +++ b/plugins/node/instrumentation-mongoose/src/mongoose.ts @@ -34,23 +34,56 @@ import { SEMATTRS_DB_SYSTEM, } from '@opentelemetry/semantic-conventions'; -const contextCaptureFunctions = [ - 'remove', +const contextCaptureFunctionsCommon = [ 'deleteOne', 'deleteMany', 'find', 'findOne', 'estimatedDocumentCount', 'countDocuments', - 'count', 'distinct', 'where', '$where', 'findOneAndUpdate', 'findOneAndDelete', 'findOneAndReplace', +]; + +const contextCaptureFunctions6 = [ + 'remove', + 'count', + 'findOneAndRemove', + ...contextCaptureFunctionsCommon, +]; +const contextCaptureFunctions7 = [ + 'count', 'findOneAndRemove', + ...contextCaptureFunctionsCommon, ]; +const contextCaptureFunctions8 = [...contextCaptureFunctionsCommon]; + +function getContextCaptureFunctions( + moduleVersion: string | undefined +): string[] { + /* istanbul ignore next */ + if (!moduleVersion) { + return contextCaptureFunctionsCommon; + } else if (moduleVersion.startsWith('6.') || moduleVersion.startsWith('5.')) { + return contextCaptureFunctions6; + } else if (moduleVersion.startsWith('7.')) { + return contextCaptureFunctions7; + } else { + return contextCaptureFunctions8; + } +} + +function instrumentRemove(moduleVersion: string | undefined): boolean { + return ( + (moduleVersion && + (moduleVersion.startsWith('5.') || moduleVersion.startsWith('6.'))) || + false + ); +} // when mongoose functions are called, we store the original call context // and then set it as the parent for the spans created by Query/Aggregate exec() @@ -65,7 +98,7 @@ export class MongooseInstrumentation extends InstrumentationBase=5.9.7 <7'], + ['>=5.9.7 <9'], this.patch.bind(this), this.unpatch.bind(this) ); @@ -87,11 +120,14 @@ export class MongooseInstrumentation extends InstrumentationBase { this._wrap( moduleExports.Query.prototype, @@ -115,11 +153,20 @@ export class MongooseInstrumentation extends InstrumentationBase { +describe('mongoose instrumentation [common]', () => { before(async () => { try { await mongoose.connect(MONGO_URI, { @@ -82,33 +82,16 @@ describe('mongoose instrumentation', () => { await User.collection.drop().catch(); }); - it('instrumenting save operation with promise', async () => { - const document = { - firstName: 'Test first name', - lastName: 'Test last name', - email: 'test@example.com', - }; - const user: IUser = new User(document); - - await user.save(); - - const spans = getTestSpans(); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('save'); - const statement = getStatement(spans[0] as ReadableSpan); - expect(statement.document).toEqual(expect.objectContaining(document)); - }); - - describe('when save call does not have callback', async () => { - it('instrumenting save operation with option property set', async () => { + describe('instrumenting save operation', async () => { + it('instrumenting save operation with promise', async () => { const document = { firstName: 'Test first name', lastName: 'Test last name', email: 'test@example.com', }; const user: IUser = new User(document); - await user.save({ wtimeout: 42 }); + + await user.save(); const spans = getTestSpans(); expect(spans.length).toBe(1); @@ -116,74 +99,29 @@ describe('mongoose instrumentation', () => { expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('save'); const statement = getStatement(spans[0] as ReadableSpan); expect(statement.document).toEqual(expect.objectContaining(document)); - expect(statement.options.wtimeout).toEqual(42); - - const createdUser = await User.findById(user._id).lean(); - expect(createdUser?._id.toString()).toEqual(user._id.toString()); - }); - }); - - describe('when save call has callback', async () => { - it('instrumenting save operation with promise and option property set', done => { - const document = { - firstName: 'Test first name', - lastName: 'Test last name', - email: 'test@example.com', - }; - const user: IUser = new User(document); - user.save({ wtimeout: 42 }, async () => { - const spans = getTestSpans(); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('save'); - const statement = getStatement(spans[0] as ReadableSpan); - expect(statement.document).toEqual(expect.objectContaining(document)); - expect(statement.options.wtimeout).toEqual(42); - - const createdUser = await User.findById(user._id).lean(); - expect(createdUser?._id.toString()).toEqual(user._id.toString()); - done(); - }); - }); - - it('instrumenting save operation with generic options and callback', done => { - const document = { - firstName: 'Test first name', - lastName: 'Test last name', - email: 'test@example.com', - }; - const user: IUser = new User(document); - - user.save({}, () => { - const spans = getTestSpans(); - - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('save'); - const statement = getStatement(spans[0] as ReadableSpan); - expect(statement.document).toEqual(expect.objectContaining(document)); - done(); - }); }); - it('instrumenting save operation with only callback', done => { + it('instrumenting save operation with option property set', async () => { const document = { firstName: 'Test first name', lastName: 'Test last name', email: 'test@example.com', }; const user: IUser = new User(document); + await user.save({ wtimeout: 42 }); - user.save(() => { - const spans = getTestSpans(); + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.document).toEqual(expect.objectContaining(document)); + expect(statement.options.wtimeout).toEqual(42); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('save'); - const statement = getStatement(spans[0] as ReadableSpan); - expect(statement.document).toEqual(expect.objectContaining(document)); - done(); - }); + const createdUser = await User.findById(user._id).lean(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - v8 made `._id` optional + expect(createdUser?._id.toString()).toEqual(user._id.toString()); }); }); @@ -234,31 +172,6 @@ describe('mongoose instrumentation', () => { }); }); - it('instrumenting remove operation [deprecated]', async () => { - const user = await User.findOne({ email: 'john.doe@example.com' }); - await user!.remove(); - - const spans = getTestSpans(); - expect(spans.length).toBe(2); - assertSpan(spans[1] as ReadableSpan); - expect(spans[1].attributes[SEMATTRS_DB_OPERATION]).toBe('remove'); - }); - - it('instrumenting remove operation with callbacks [deprecated]', done => { - User.findOne({ email: 'john.doe@example.com' }).then(user => - user!.remove({ overwrite: true }, () => { - const spans = getTestSpans(); - expect(spans.length).toBe(2); - assertSpan(spans[1] as ReadableSpan); - expect(spans[1].attributes[SEMATTRS_DB_OPERATION]).toBe('remove'); - expect(getStatement(spans[1] as ReadableSpan).options).toEqual({ - overwrite: true, - }); - done(); - }) - ); - }); - it('instrumenting deleteOne operation', async () => { await User.deleteOne({ email: 'john.doe@example.com' }); @@ -301,18 +214,6 @@ describe('mongoose instrumentation', () => { expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); }); - it('instrumenting count operation [deprecated]', async () => { - await User.count({}); - - const spans = getTestSpans(); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('count'); - const statement = getStatement(spans[0] as ReadableSpan); - expect(statement.options).toEqual({}); - expect(statement.condition).toEqual({}); - }); - it('instrumenting countDocuments operation', async () => { await User.countDocuments({ email: 'john.doe@example.com' }); @@ -363,22 +264,6 @@ describe('mongoose instrumentation', () => { expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); }); - it('instrumenting update operation [deprecated]', async () => { - await User.update( - { email: 'john.doe@example.com' }, - { email: 'john.doe2@example.com' } - ); - - const spans = getTestSpans(); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('update'); - const statement = getStatement(spans[0] as ReadableSpan); - expect(statement.options).toEqual({}); - expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); - expect(statement.updates).toEqual({ email: 'john.doe2@example.com' }); - }); - it('instrumenting updateOne operation', async () => { await User.updateOne({ email: 'john.doe@example.com' }, { age: 55 }); @@ -417,36 +302,6 @@ describe('mongoose instrumentation', () => { expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); }); - it('instrumenting findOneAndUpdate operation', async () => { - await User.findOneAndUpdate( - { email: 'john.doe@example.com' }, - { isUpdated: true } - ); - - const spans = getTestSpans(); - expect(spans.length).toBe(2); - assertSpan(spans[0] as ReadableSpan); - assertSpan(spans[1] as ReadableSpan); - expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('findOne'); - expect(spans[1].attributes[SEMATTRS_DB_OPERATION]).toBe('findOneAndUpdate'); - const statement = getStatement(spans[1] as ReadableSpan); - expect(statement.options).toEqual({}); - expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); - expect(statement.updates).toEqual({ isUpdated: true }); - }); - - it('instrumenting findOneAndRemove operation', async () => { - await User.findOneAndRemove({ email: 'john.doe@example.com' }); - - const spans = getTestSpans(); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('findOneAndRemove'); - const statement = getStatement(spans[0] as ReadableSpan); - expect(statement.options).toEqual({}); - expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); - }); - it('instrumenting create operation', async () => { const document = { firstName: 'John', @@ -481,27 +336,6 @@ describe('mongoose instrumentation', () => { ]); }); - it('instrumenting aggregate operation with callback', done => { - User.aggregate( - [ - { $match: { firstName: 'John' } }, - { $group: { _id: 'John', total: { $sum: '$amount' } } }, - ], - () => { - const spans = getTestSpans(); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('aggregate'); - const statement = getStatement(spans[0] as ReadableSpan); - expect(statement.aggregatePipeline).toEqual([ - { $match: { firstName: 'John' } }, - { $group: { _id: 'John', total: { $sum: '$amount' } } }, - ]); - done(); - } - ); - }); - it('instrumenting combined operation with async/await', async () => { await User.find({ id: '_test' }).skip(1).limit(2).sort({ email: 'asc' }); @@ -572,20 +406,6 @@ describe('mongoose instrumentation', () => { ); }); - it('responseHook works with callback in exec patch', done => { - User.deleteOne({ email: 'john.doe@example.com' }, { lean: 1 }, () => { - const spans = getTestSpans(); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect( - JSON.parse(spans[0].attributes[RESPONSE] as string) - ).toMatchObject({ - deletedCount: 1, - }); - done(); - }); - }); - it('responseHook works with async/await in model methods patch', async () => { const document = { firstName: 'Test first name', @@ -602,24 +422,6 @@ describe('mongoose instrumentation', () => { ); }); - it('responseHook works with callback in model methods patch', done => { - const document = { - firstName: 'Test first name', - lastName: 'Test last name', - email: 'test@example.com', - }; - const user: IUser = new User(document); - user.save((_err, createdUser) => { - const spans = getTestSpans(); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(spans[0].attributes[RESPONSE]).toEqual( - JSON.stringify(createdUser) - ); - done(); - }); - }); - it('responseHook works with async/await in aggregate patch', async () => { await User.aggregate([ { $match: { firstName: 'John' } }, @@ -634,24 +436,6 @@ describe('mongoose instrumentation', () => { ]); }); - it('responseHook works with callback in aggregate patch', done => { - User.aggregate( - [ - { $match: { firstName: 'John' } }, - { $group: { _id: 'John', total: { $sum: '$amount' } } }, - ], - () => { - const spans = getTestSpans(); - expect(spans.length).toBe(1); - assertSpan(spans[0] as ReadableSpan); - expect(JSON.parse(spans[0].attributes[RESPONSE] as string)).toEqual([ - { _id: 'John', total: 0 }, - ]); - done(); - } - ); - }); - it('error in response hook does not fail anything', async () => { instrumentation.disable(); instrumentation.setConfig({ diff --git a/plugins/node/instrumentation-mongoose/test/mongoose-v5-v6.test.ts b/plugins/node/instrumentation-mongoose/test/mongoose-v5-v6.test.ts new file mode 100644 index 0000000000..1e2fe97740 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/mongoose-v5-v6.test.ts @@ -0,0 +1,338 @@ +/* + * 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. + */ +import 'mocha'; +import { expect } from 'expect'; +import { SEMATTRS_DB_OPERATION } from '@opentelemetry/semantic-conventions'; +import { MongooseInstrumentation } from '../src'; +import { + getTestSpans, + registerInstrumentationTesting, +} from '@opentelemetry/contrib-test-utils'; +import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; + +const instrumentation = registerInstrumentationTesting( + new MongooseInstrumentation() +); + +import * as mongoose from 'mongoose'; +import User, { IUser, loadUsers } from './user'; +import { assertSpan, getStatement } from './asserts'; +import { DB_NAME, MONGO_URI } from './config'; + +// We can't use @ts-expect-error because it will fail depending on the used mongoose version on tests +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +// Please run mongodb in the background: docker run -d -p 27017:27017 -v ~/data:/data/db mongo +describe('mongoose instrumentation [v5/v6]', () => { + before(async () => { + try { + await mongoose.connect(MONGO_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + useFindAndModify: false, + useCreateIndex: true, + dbName: DB_NAME, + } as any); // TODO: amir - document older mongoose support + } catch (err: any) { + // connect signature changed from mongo v5 to v6. + // the following check tries both signatures, so test-all-versions + // can run against both versions. + if (err?.name === 'MongoParseError') { + await mongoose.connect(MONGO_URI, { + dbName: DB_NAME, + }); // TODO: amir - document older mongoose support + } + } + }); + + after(async () => { + await mongoose.connection.close(); + }); + + beforeEach(async () => { + instrumentation.disable(); + instrumentation.setConfig({ + dbStatementSerializer: (_operation: string, payload) => { + return JSON.stringify(payload, (key, value) => { + return key === 'session' ? '[Session]' : value; + }); + }, + }); + instrumentation.enable(); + await loadUsers(); + await User.createIndexes(); + }); + + afterEach(async () => { + instrumentation.disable(); + await User.collection.drop().catch(); + }); + + describe('when save call has callback', async () => { + it('instrumenting save operation with promise and option property set', done => { + const document = { + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + // @ts-ignore - v7 removed callback support + // https://mongoosejs.com/docs/migrating_to_7.html#dropped-callback-support + user.save({ wtimeout: 42 }, async () => { + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.document).toEqual(expect.objectContaining(document)); + expect(statement.options.wtimeout).toEqual(42); + + const createdUser = await User.findById(user._id).lean(); + // @ts-ignore - v8 made `._id` optional + // https://mongoosejs.com/docs/migrating_to_8.html#removed-id-setter + expect(createdUser?._id.toString()).toEqual(user._id.toString()); + done(); + }); + }); + + it('instrumenting save operation with generic options and callback', done => { + const document = { + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + + // @ts-ignore - v7 removed callback support + // https://mongoosejs.com/docs/migrating_to_7.html#dropped-callback-support + user.save({}, () => { + const spans = getTestSpans(); + + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.document).toEqual(expect.objectContaining(document)); + done(); + }); + }); + + it('instrumenting save operation with only callback', done => { + const document = { + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + + // @ts-ignore - v7 removed callback support + user.save(() => { + const spans = getTestSpans(); + + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.document).toEqual(expect.objectContaining(document)); + done(); + }); + }); + }); + + describe('remove operation', () => { + it('instrumenting remove operation [deprecated]', async () => { + const user = await User.findOne({ email: 'john.doe@example.com' }); + // @ts-ignore - v7 removed `remove` method + // https://mongoosejs.com/docs/migrating_to_7.html#removed-remove + await user!.remove(); + + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[1] as ReadableSpan); + expect(spans[1].attributes[SEMATTRS_DB_OPERATION]).toBe('remove'); + }); + + it('instrumenting remove operation with callbacks [deprecated]', done => { + User.findOne({ email: 'john.doe@example.com' }).then(user => + // @ts-ignore - v7 removed `remove` method + // https://mongoosejs.com/docs/migrating_to_7.html#removed-remove + user!.remove({ overwrite: true }, () => { + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[1] as ReadableSpan); + expect(spans[1].attributes[SEMATTRS_DB_OPERATION]).toBe('remove'); + expect(getStatement(spans[1] as ReadableSpan).options).toEqual({ + overwrite: true, + }); + done(); + }) + ); + }); + }); + + it('instrumenting count operation [deprecated]', async () => { + // @ts-ignore - v8 removed `count` method + // https://mongoosejs.com/docs/migrating_to_8.html#removed-count + await User.count({}); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('count'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({}); + }); + + it('instrumenting update operation [deprecated]', async () => { + // @ts-ignore - v7 removed `update` method + // https://mongoosejs.com/docs/migrating_to_7.html#removed-update + await User.update( + { email: 'john.doe@example.com' }, + { email: 'john.doe2@example.com' } + ); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('update'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + expect(statement.updates).toEqual({ email: 'john.doe2@example.com' }); + }); + + it('instrumenting findOneAndUpdate operation', async () => { + await User.findOneAndUpdate( + { email: 'john.doe@example.com' }, + { isUpdated: true } + ); + + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[0] as ReadableSpan); + assertSpan(spans[1] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('findOne'); + expect(spans[1].attributes[SEMATTRS_DB_OPERATION]).toBe('findOneAndUpdate'); + const statement = getStatement(spans[1] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + expect(statement.updates).toEqual({ isUpdated: true }); + }); + + it('instrumenting findOneAndRemove operation', async () => { + // @ts-ignore - v8 removed `findOneAndRemove` method + // https://mongoosejs.com/docs/migrating_to_8.html#removed-findoneandremove + await User.findOneAndRemove({ email: 'john.doe@example.com' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('findOneAndRemove'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + }); + + it('instrumenting aggregate operation with callback', done => { + User.aggregate( + [ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ], + () => { + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('aggregate'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.aggregatePipeline).toEqual([ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ]); + done(); + } + ); + }); + + describe('responseHook', () => { + const RESPONSE = 'db.response'; + beforeEach(() => { + instrumentation.disable(); + instrumentation.setConfig({ + responseHook: (span, responseInfo) => + span.setAttribute(RESPONSE, JSON.stringify(responseInfo.response)), + }); + instrumentation.enable(); + }); + + it('responseHook works with callback in exec patch', done => { + // @ts-ignore - v7 removed callback support + // https://mongoosejs.com/docs/migrating_to_7.html#dropped-callback-support + User.deleteOne({ email: 'john.doe@example.com' }, { lean: 1 }, () => { + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect( + JSON.parse(spans[0].attributes[RESPONSE] as string) + ).toMatchObject({ + deletedCount: 1, + }); + done(); + }); + }); + + it('responseHook works with callback in model methods patch', done => { + const document = { + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + // @ts-ignore - v7 removed callback support + // https://mongoosejs.com/docs/migrating_to_7.html#dropped-callback-support + user.save((_err, createdUser) => { + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[RESPONSE]).toEqual( + JSON.stringify(createdUser) + ); + done(); + }); + }); + + it('responseHook works with callback in aggregate patch', done => { + User.aggregate( + [ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ], + () => { + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(JSON.parse(spans[0].attributes[RESPONSE] as string)).toEqual([ + { _id: 'John', total: 0 }, + ]); + done(); + } + ); + }); + }); +}); + +/* eslint-enable @typescript-eslint/ban-ts-comment */ diff --git a/plugins/node/instrumentation-mongoose/test/mongoose-v7-v8.test.ts b/plugins/node/instrumentation-mongoose/test/mongoose-v7-v8.test.ts new file mode 100644 index 0000000000..5324e84b6f --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/mongoose-v7-v8.test.ts @@ -0,0 +1,96 @@ +/* + * 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. + */ +import 'mocha'; +import { expect } from 'expect'; +import { SEMATTRS_DB_OPERATION } from '@opentelemetry/semantic-conventions'; +import { MongooseInstrumentation } from '../src'; +import { + getTestSpans, + registerInstrumentationTesting, +} from '@opentelemetry/contrib-test-utils'; +import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; + +const instrumentation = registerInstrumentationTesting( + new MongooseInstrumentation() +); + +import * as mongoose from 'mongoose'; +import User, { loadUsers } from './user'; +import { assertSpan, getStatement } from './asserts'; +import { DB_NAME, MONGO_URI } from './config'; + +// Please run mongodb in the background: docker run -d -p 27017:27017 -v ~/data:/data/db mongo +describe('mongoose instrumentation [v7/v8]', () => { + before(async () => { + try { + await mongoose.connect(MONGO_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + useFindAndModify: false, + useCreateIndex: true, + dbName: DB_NAME, + } as any); // TODO: amir - document older mongoose support + } catch (err: any) { + // connect signature changed from mongo v5 to v6. + // the following check tries both signatures, so test-all-versions + // can run against both versions. + if (err?.name === 'MongoParseError') { + await mongoose.connect(MONGO_URI, { + dbName: DB_NAME, + }); // TODO: amir - document older mongoose support + } + } + }); + + after(async () => { + await mongoose.connection.close(); + }); + + beforeEach(async () => { + instrumentation.disable(); + instrumentation.setConfig({ + dbStatementSerializer: (_operation: string, payload) => { + return JSON.stringify(payload, (key, value) => { + return key === 'session' ? '[Session]' : value; + }); + }, + }); + instrumentation.enable(); + await loadUsers(); + await User.createIndexes(); + }); + + afterEach(async () => { + instrumentation.disable(); + await User.collection.drop().catch(); + }); + + it('instrumenting findOneAndUpdate operation', async () => { + await User.findOneAndUpdate( + { email: 'john.doe@example.com' }, + { isUpdated: true } + ); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SEMATTRS_DB_OPERATION]).toBe('findOneAndUpdate'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + expect(statement.updates).toEqual({ isUpdated: true }); + }); +});