From cf02736b3fc6574faa9521de117171e93314efde Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 28 Aug 2022 13:03:46 +0300 Subject: [PATCH 1/7] feat: add mongoose instrumentation --- .github/component_owners.yml | 2 + .release-please-manifest.json | 2 +- .../auto-instrumentations-node/README.md | 1 + .../auto-instrumentations-node/package.json | 1 + .../auto-instrumentations-node/src/utils.ts | 2 + .../test/utils.test.ts | 3 +- .../instrumentation-mongoose/.eslintignore | 1 + .../instrumentation-mongoose/.eslintrc.js | 7 + .../node/instrumentation-mongoose/.npmignore | 4 + .../node/instrumentation-mongoose/.tav.yml | 4 + plugins/node/instrumentation-mongoose/LICENSE | 201 +++++ .../node/instrumentation-mongoose/README.md | 71 ++ .../instrumentation-mongoose/package.json | 69 ++ .../instrumentation-mongoose/src/index.ts | 17 + .../instrumentation-mongoose/src/mongoose.ts | 369 +++++++++ .../instrumentation-mongoose/src/types.ts | 62 ++ .../instrumentation-mongoose/src/utils.ts | 107 +++ .../instrumentation-mongoose/test/asserts.ts | 35 + .../instrumentation-mongoose/test/config.ts | 20 + .../test/mongoose.test.ts | 726 ++++++++++++++++++ .../instrumentation-mongoose/test/user.ts | 59 ++ .../instrumentation-mongoose/tsconfig.json | 12 + release-please-config.json | 1 + 23 files changed, 1774 insertions(+), 2 deletions(-) create mode 100644 plugins/node/instrumentation-mongoose/.eslintignore create mode 100644 plugins/node/instrumentation-mongoose/.eslintrc.js create mode 100644 plugins/node/instrumentation-mongoose/.npmignore create mode 100644 plugins/node/instrumentation-mongoose/.tav.yml create mode 100644 plugins/node/instrumentation-mongoose/LICENSE create mode 100644 plugins/node/instrumentation-mongoose/README.md create mode 100644 plugins/node/instrumentation-mongoose/package.json create mode 100644 plugins/node/instrumentation-mongoose/src/index.ts create mode 100644 plugins/node/instrumentation-mongoose/src/mongoose.ts create mode 100644 plugins/node/instrumentation-mongoose/src/types.ts create mode 100644 plugins/node/instrumentation-mongoose/src/utils.ts create mode 100644 plugins/node/instrumentation-mongoose/test/asserts.ts create mode 100644 plugins/node/instrumentation-mongoose/test/config.ts create mode 100644 plugins/node/instrumentation-mongoose/test/mongoose.test.ts create mode 100644 plugins/node/instrumentation-mongoose/test/user.ts create mode 100644 plugins/node/instrumentation-mongoose/tsconfig.json diff --git a/.github/component_owners.yml b/.github/component_owners.yml index abaf53f05b..4461169979 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -42,6 +42,8 @@ components: - blumamir plugins/node/opentelemetry-instrumentation-memcached: - rauno56 + plugins/node/opentelemetry-instrumentation-mongoose: + - blumamir plugins/node/opentelemetry-instrumentation-nestjs-core: - rauno56 plugins/node/opentelemetry-instrumentation-redis: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3455c2f002..b8dad09b0f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.1","detectors/node/opentelemetry-resource-detector-aws":"1.1.1","detectors/node/opentelemetry-resource-detector-gcp":"0.27.1","detectors/node/opentelemetry-resource-detector-github":"0.27.0","metapackages/auto-instrumentations-node":"0.31.2","metapackages/auto-instrumentations-web":"0.29.1","packages/opentelemetry-host-metrics":"0.29.0","packages/opentelemetry-id-generator-aws-xray":"1.1.0","packages/opentelemetry-test-utils":"0.31.0","plugins/node/instrumentation-amqplib":"0.30.0","plugins/node/instrumentation-fs":"0.4.0","plugins/node/instrumentation-tedious":"0.3.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.32.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.8.1","plugins/node/opentelemetry-instrumentation-bunyan":"0.29.0","plugins/node/opentelemetry-instrumentation-cassandra":"0.29.1","plugins/node/opentelemetry-instrumentation-connect":"0.29.0","plugins/node/opentelemetry-instrumentation-dns":"0.29.0","plugins/node/opentelemetry-instrumentation-express":"0.30.0","plugins/node/opentelemetry-instrumentation-generic-pool":"0.29.0","plugins/node/opentelemetry-instrumentation-graphql":"0.29.0","plugins/node/opentelemetry-instrumentation-hapi":"0.29.0","plugins/node/opentelemetry-instrumentation-ioredis":"0.31.0","plugins/node/opentelemetry-instrumentation-knex":"0.29.1","plugins/node/opentelemetry-instrumentation-koa":"0.31.0","plugins/node/instrumentation-lru-memoizer":"0.30.0","plugins/node/opentelemetry-instrumentation-memcached":"0.29.0","plugins/node/opentelemetry-instrumentation-mongodb":"0.31.1","plugins/node/opentelemetry-instrumentation-mysql":"0.30.0","plugins/node/opentelemetry-instrumentation-mysql2":"0.31.1","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.30.0","plugins/node/opentelemetry-instrumentation-net":"0.29.0","plugins/node/opentelemetry-instrumentation-pg":"0.30.0","plugins/node/opentelemetry-instrumentation-pino":"0.30.0","plugins/node/opentelemetry-instrumentation-redis":"0.32.0","plugins/node/opentelemetry-instrumentation-redis-4":"0.31.0","plugins/node/opentelemetry-instrumentation-restify":"0.29.0","plugins/node/opentelemetry-instrumentation-router":"0.29.1","plugins/node/opentelemetry-instrumentation-winston":"0.29.0","plugins/web/opentelemetry-instrumentation-document-load":"0.29.0","plugins/web/opentelemetry-instrumentation-user-interaction":"0.30.1","plugins/web/opentelemetry-plugin-react-load":"0.28.0","propagators/opentelemetry-propagator-aws-xray":"1.1.0","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.0","propagators/opentelemetry-propagator-instana":"0.2.0","propagators/opentelemetry-propagator-ot-trace":"0.26.1","plugins/node/opentelemetry-instrumentation-fastify":"0.28.0","packages/opentelemetry-propagation-utils":"0.28.0","plugins/web/opentelemetry-instrumentation-long-task":"0.30.0","detectors/node/opentelemetry-resource-detector-docker":"0.1.1","detectors/node/opentelemetry-resource-detector-instana":"0.2.0"} +{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.1","detectors/node/opentelemetry-resource-detector-aws":"1.1.1","detectors/node/opentelemetry-resource-detector-gcp":"0.27.1","detectors/node/opentelemetry-resource-detector-github":"0.27.0","metapackages/auto-instrumentations-node":"0.31.2","metapackages/auto-instrumentations-web":"0.29.1","packages/opentelemetry-host-metrics":"0.29.0","packages/opentelemetry-id-generator-aws-xray":"1.1.0","packages/opentelemetry-test-utils":"0.31.0","plugins/node/instrumentation-amqplib":"0.30.0","plugins/node/instrumentation-fs":"0.4.0","plugins/node/instrumentation-tedious":"0.3.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.32.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.8.1","plugins/node/opentelemetry-instrumentation-bunyan":"0.29.0","plugins/node/opentelemetry-instrumentation-cassandra":"0.29.1","plugins/node/opentelemetry-instrumentation-connect":"0.29.0","plugins/node/opentelemetry-instrumentation-dns":"0.29.0","plugins/node/opentelemetry-instrumentation-express":"0.30.0","plugins/node/opentelemetry-instrumentation-generic-pool":"0.29.0","plugins/node/opentelemetry-instrumentation-graphql":"0.29.0","plugins/node/opentelemetry-instrumentation-hapi":"0.29.0","plugins/node/opentelemetry-instrumentation-ioredis":"0.31.0","plugins/node/opentelemetry-instrumentation-knex":"0.29.1","plugins/node/opentelemetry-instrumentation-koa":"0.31.0","plugins/node/instrumentation-lru-memoizer":"0.30.0","plugins/node/instrumentation-mongoose":"0.30.0","plugins/node/opentelemetry-instrumentation-memcached":"0.29.0","plugins/node/opentelemetry-instrumentation-mongodb":"0.31.1","plugins/node/opentelemetry-instrumentation-mysql":"0.30.0","plugins/node/opentelemetry-instrumentation-mysql2":"0.31.1","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.30.0","plugins/node/opentelemetry-instrumentation-net":"0.29.0","plugins/node/opentelemetry-instrumentation-pg":"0.30.0","plugins/node/opentelemetry-instrumentation-pino":"0.30.0","plugins/node/opentelemetry-instrumentation-redis":"0.32.0","plugins/node/opentelemetry-instrumentation-redis-4":"0.31.0","plugins/node/opentelemetry-instrumentation-restify":"0.29.0","plugins/node/opentelemetry-instrumentation-router":"0.29.1","plugins/node/opentelemetry-instrumentation-winston":"0.29.0","plugins/web/opentelemetry-instrumentation-document-load":"0.29.0","plugins/web/opentelemetry-instrumentation-user-interaction":"0.30.1","plugins/web/opentelemetry-plugin-react-load":"0.28.0","propagators/opentelemetry-propagator-aws-xray":"1.1.0","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.0","propagators/opentelemetry-propagator-instana":"0.2.0","propagators/opentelemetry-propagator-ot-trace":"0.26.1","plugins/node/opentelemetry-instrumentation-fastify":"0.28.0","packages/opentelemetry-propagation-utils":"0.28.0","plugins/web/opentelemetry-instrumentation-long-task":"0.30.0","detectors/node/opentelemetry-resource-detector-docker":"0.1.1","detectors/node/opentelemetry-resource-detector-instana":"0.2.0"} diff --git a/metapackages/auto-instrumentations-node/README.md b/metapackages/auto-instrumentations-node/README.md index 37f850a03b..72cc4c7ffa 100644 --- a/metapackages/auto-instrumentations-node/README.md +++ b/metapackages/auto-instrumentations-node/README.md @@ -78,6 +78,7 @@ registerInstrumentations({ - [@opentelemetry/instrumentation-koa](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-koa) - [@opentelemetry/instrumentation-memcached](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-memcached) - [@opentelemetry/instrumentation-mongodb](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mongodb) +- [@opentelemetry/instrumentation-mongoose](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-mongoose) - [@opentelemetry/instrumentation-mysql2](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mysql2) - [@opentelemetry/instrumentation-mysql](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mysql) - [@opentelemetry/instrumentation-nestjs-core](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-nestjs-core) diff --git a/metapackages/auto-instrumentations-node/package.json b/metapackages/auto-instrumentations-node/package.json index e2ecdbefd9..b3252c335c 100644 --- a/metapackages/auto-instrumentations-node/package.json +++ b/metapackages/auto-instrumentations-node/package.json @@ -67,6 +67,7 @@ "@opentelemetry/instrumentation-lru-memoizer": "^0.30.0", "@opentelemetry/instrumentation-memcached": "^0.29.0", "@opentelemetry/instrumentation-mongodb": "^0.31.1", + "@opentelemetry/instrumentation-mongoose": "^0.30.0", "@opentelemetry/instrumentation-mysql": "^0.30.0", "@opentelemetry/instrumentation-mysql2": "^0.31.1", "@opentelemetry/instrumentation-nestjs-core": "^0.30.0", diff --git a/metapackages/auto-instrumentations-node/src/utils.ts b/metapackages/auto-instrumentations-node/src/utils.ts index 44163eaac8..f85f42eee8 100644 --- a/metapackages/auto-instrumentations-node/src/utils.ts +++ b/metapackages/auto-instrumentations-node/src/utils.ts @@ -36,6 +36,7 @@ import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; import { LruMemoizerInstrumentation } from '@opentelemetry/instrumentation-lru-memoizer'; import { MemcachedInstrumentation } from '@opentelemetry/instrumentation-memcached'; import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; +import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2'; import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; @@ -69,6 +70,7 @@ const InstrumentationMap = { '@opentelemetry/instrumentation-lru-memoizer': LruMemoizerInstrumentation, '@opentelemetry/instrumentation-memcached': MemcachedInstrumentation, '@opentelemetry/instrumentation-mongodb': MongoDBInstrumentation, + '@opentelemetry/instrumentation-mongoose': MongooseInstrumentation, '@opentelemetry/instrumentation-mysql2': MySQL2Instrumentation, '@opentelemetry/instrumentation-mysql': MySQLInstrumentation, '@opentelemetry/instrumentation-nestjs-core': NestInstrumentation, diff --git a/metapackages/auto-instrumentations-node/test/utils.test.ts b/metapackages/auto-instrumentations-node/test/utils.test.ts index 20f15c1723..24d8d561ab 100644 --- a/metapackages/auto-instrumentations-node/test/utils.test.ts +++ b/metapackages/auto-instrumentations-node/test/utils.test.ts @@ -45,6 +45,7 @@ describe('utils', () => { '@opentelemetry/instrumentation-lru-memoizer', '@opentelemetry/instrumentation-memcached', '@opentelemetry/instrumentation-mongodb', + '@opentelemetry/instrumentation-mongoose', '@opentelemetry/instrumentation-mysql2', '@opentelemetry/instrumentation-mysql', '@opentelemetry/instrumentation-nestjs-core', @@ -56,7 +57,7 @@ describe('utils', () => { '@opentelemetry/instrumentation-restify', '@opentelemetry/instrumentation-winston', ]; - assert.strictEqual(instrumentations.length, 30); + assert.strictEqual(instrumentations.length, 31); for (let i = 0, j = instrumentations.length; i < j; i++) { assert.strictEqual( instrumentations[i].instrumentationName, diff --git a/plugins/node/instrumentation-mongoose/.eslintignore b/plugins/node/instrumentation-mongoose/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/.eslintignore @@ -0,0 +1 @@ +build diff --git a/plugins/node/instrumentation-mongoose/.eslintrc.js b/plugins/node/instrumentation-mongoose/.eslintrc.js new file mode 100644 index 0000000000..f756f4488b --- /dev/null +++ b/plugins/node/instrumentation-mongoose/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.config.js') +} diff --git a/plugins/node/instrumentation-mongoose/.npmignore b/plugins/node/instrumentation-mongoose/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/plugins/node/instrumentation-mongoose/.tav.yml b/plugins/node/instrumentation-mongoose/.tav.yml new file mode 100644 index 0000000000..8367cdde2e --- /dev/null +++ b/plugins/node/instrumentation-mongoose/.tav.yml @@ -0,0 +1,4 @@ +'mongoose': + versions: ">=5.9.7 <7" + commands: + - npm run test diff --git a/plugins/node/instrumentation-mongoose/LICENSE b/plugins/node/instrumentation-mongoose/LICENSE new file mode 100644 index 0000000000..e50e8c80f9 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2022] OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/node/instrumentation-mongoose/README.md b/plugins/node/instrumentation-mongoose/README.md new file mode 100644 index 0000000000..ebd17e0745 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/README.md @@ -0,0 +1,71 @@ +# OpenTelemetry mongoose Instrumentation for Node.js + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +This module provides automatic instrumentation for the [`mongoose`](https://github.com/Automattic/mongoose) module, which may be loaded using the [`@opentelemetry/sdk-trace-node`](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node) package and is included in the [`@opentelemetry/auto-instrumentations-node`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) bundle. + +If total installation size is not constrained, it is recommended to use the [`@opentelemetry/auto-instrumentations-node`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) bundle with [@opentelemetry/sdk-node](`https://www.npmjs.com/package/@opentelemetry/sdk-node`) for the most seamless instrumentation experience. + +Compatible with OpenTelemetry JS API and SDK `1.0+`. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-mongoose +``` + +## Supported Versions + +- `>=5.9.7 <7` + +## Usage + +To load a specific plugin, specify it in the registerInstrumentations's configuration: + +```js +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const { MongooseInstrumentation } = require('@opentelemetry/instrumentation-mongoose'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + new MongooseInstrumentation(), + ], +}) +``` + +## Migration From opentelemetry-instrumentation-mongoose + +This instrumentation was originally published under the name [`"@wdalmut/opentelemetry-plugin-mongoose"`] in [this repo](https://github.com/wdalmut/opentelemetry-plugin-mongoose). + +It was then ported and maintained under the name `"opentelemetry-instrumentation-mongoose"` in [this repo](https://github.com/aspecto-io/opentelemetry-ext-js). + +Few breaking changes were made during porting to the contrib repo to align with conventions: + +### Hook Info + +The instrumentation's config `responseHook` functions signature changed, so the second function parameter is info object, containing the relevant hook data. + +### `moduleVersionAttributeName` config option + +The `moduleVersionAttributeName` config option is removed. To add the mongoose package version to spans, use the `moduleVersion` attribute in hook info for `responseHook` function. + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[license-url]: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-mongoose +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-mongoose.svg diff --git a/plugins/node/instrumentation-mongoose/package.json b/plugins/node/instrumentation-mongoose/package.json new file mode 100644 index 0000000000..203943dfe8 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/package.json @@ -0,0 +1,69 @@ +{ + "name": "@opentelemetry/instrumentation-mongoose", + "version": "0.30.0", + "description": "OpenTelemetry automatic instrumentation package for mongoose", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "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-all-versions": "tav", + "tdd": "npm run test -- --watch-extensions ts --watch", + "clean": "rimraf build/*", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "precompile": "tsc --version && lerna run version --scope @opentelemetry/instrumentation-mongoose --include-dependencies", + "prewatch": "npm run precompile", + "prepare": "npm run compile", + "version:update": "node ../../../scripts/version-update.js", + "compile": "npm run version:update && tsc -p ." + }, + "keywords": [ + "mongodb", + "mongoose", + "orm", + "instrumentation", + "nodejs", + "opentelemetry", + "tracing" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "devDependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/contrib-test-utils": "^0.31.0", + "@opentelemetry/sdk-trace-base": "^1.3.1", + "@types/mocha": "8.2.3", + "@types/node": "16.11.21", + "expect": "27.4.2", + "gts": "3.1.0", + "mocha": "7.2.0", + "mongoose": "^6.5.3", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "test-all-versions": "5.0.1", + "ts-mocha": "10.0.0", + "typescript": "4.3.5" + }, + "dependencies": { + "@opentelemetry/core": "^1.0.0", + "@opentelemetry/instrumentation": "^0.31.0", + "@opentelemetry/semantic-conventions": "^1.0.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-mongoose#readme" +} diff --git a/plugins/node/instrumentation-mongoose/src/index.ts b/plugins/node/instrumentation-mongoose/src/index.ts new file mode 100644 index 0000000000..5a9c213a2b --- /dev/null +++ b/plugins/node/instrumentation-mongoose/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ +export * from './mongoose'; +export * from './types'; diff --git a/plugins/node/instrumentation-mongoose/src/mongoose.ts b/plugins/node/instrumentation-mongoose/src/mongoose.ts new file mode 100644 index 0000000000..67a170ba1b --- /dev/null +++ b/plugins/node/instrumentation-mongoose/src/mongoose.ts @@ -0,0 +1,369 @@ +/* + * 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 { + context, + Span, + trace, + SpanAttributes, + SpanKind, +} from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; +import type * as mongoose from 'mongoose'; +import { MongooseInstrumentationConfig, SerializerPayload } from './types'; +import { + handleCallbackResponse, + handlePromiseResponse, + getAttributesFromCollection, +} from './utils'; +import { + InstrumentationBase, + InstrumentationModuleDefinition, + InstrumentationNodeModuleDefinition, +} from '@opentelemetry/instrumentation'; +import { VERSION } from './version'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +const contextCaptureFunctions = [ + 'remove', + 'deleteOne', + 'deleteMany', + 'find', + 'findOne', + 'estimatedDocumentCount', + 'countDocuments', + 'count', + 'distinct', + 'where', + '$where', + 'findOneAndUpdate', + 'findOneAndDelete', + 'findOneAndReplace', + 'findOneAndRemove', +]; + +// 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() +// calls. this bypass the unlinked spans issue on thenables await operations. +export const _STORED_PARENT_SPAN: unique symbol = Symbol('stored-parent-span'); + +export class MongooseInstrumentation extends InstrumentationBase { + protected override _config!: MongooseInstrumentationConfig; + + constructor(config: MongooseInstrumentationConfig = {}) { + super( + '@opentelemetry/instrumentation-mongoose', + VERSION, + Object.assign({}, config) + ); + } + + override setConfig(config: MongooseInstrumentationConfig = {}) { + this._config = Object.assign({}, config); + } + + protected init(): InstrumentationModuleDefinition { + const module = new InstrumentationNodeModuleDefinition( + 'mongoose', + ['>=5.9.7 <7'], + this.patch.bind(this), + this.unpatch.bind(this) + ); + return module; + } + + protected patch( + moduleExports: typeof mongoose, + moduleVersion: string | undefined + ) { + this._wrap( + moduleExports.Model.prototype, + 'save', + this.patchOnModelMethods('save', moduleVersion) + ); + // mongoose applies this code on moudle require: + // Model.prototype.$save = Model.prototype.save; + // which captures the save function before it is patched. + // so we need to apply the same logic after instrumenting the save function. + moduleExports.Model.prototype.$save = moduleExports.Model.prototype.save; + + this._wrap( + moduleExports.Model.prototype, + 'remove', + this.patchOnModelMethods('remove', moduleVersion) + ); + this._wrap( + moduleExports.Query.prototype, + 'exec', + this.patchQueryExec(moduleVersion) + ); + this._wrap( + moduleExports.Aggregate.prototype, + 'exec', + this.patchAggregateExec(moduleVersion) + ); + + contextCaptureFunctions.forEach((funcName: string) => { + this._wrap( + moduleExports.Query.prototype, + funcName as any, + this.patchAndCaptureSpanContext(funcName) + ); + }); + this._wrap(moduleExports.Model, 'aggregate', this.patchModelAggregate()); + + return moduleExports; + } + + protected unpatch(moduleExports: typeof mongoose): void { + this._diag.debug('mongoose instrumentation: unpatch mongoose'); + this._unwrap(moduleExports.Model.prototype, 'save'); + // revert the patch for $save which we applyed by aliasing it to patched `save` + moduleExports.Model.prototype.$save = moduleExports.Model.prototype.save; + this._unwrap(moduleExports.Model.prototype, 'remove'); + this._unwrap(moduleExports.Query.prototype, 'exec'); + this._unwrap(moduleExports.Aggregate.prototype, 'exec'); + + contextCaptureFunctions.forEach((funcName: string) => { + this._unwrap(moduleExports.Query.prototype, funcName as any); + }); + this._unwrap(moduleExports.Model, 'aggregate'); + } + + private patchAggregateExec(moduleVersion: string | undefined) { + const self = this; + this._diag.debug('patched mongoose Aggregate exec function'); + return (originalAggregate: Function) => { + return function exec(this: any, callback?: Function) { + if ( + self._config.requireParentSpan && + trace.getSpan(context.active()) === undefined + ) { + return originalAggregate.apply(this, arguments); + } + + const parentSpan = this[_STORED_PARENT_SPAN]; + const attributes: SpanAttributes = {}; + if (self._config.dbStatementSerializer) { + attributes[SemanticAttributes.DB_STATEMENT] = + self._config.dbStatementSerializer('aggregate', { + options: this.options, + aggregatePipeline: this._pipeline, + }); + } + + const span = self._startSpan( + this._model.collection, + this._model?.modelName, + 'aggregate', + attributes, + parentSpan + ); + + return self._handleResponse( + span, + originalAggregate, + this, + arguments, + callback, + moduleVersion + ); + }; + }; + } + + private patchQueryExec(moduleVersion: string | undefined) { + const self = this; + this._diag.debug('patched mongoose Query exec function'); + return (originalExec: Function) => { + return function exec(this: any, callback?: Function) { + if ( + self._config.requireParentSpan && + trace.getSpan(context.active()) === undefined + ) { + return originalExec.apply(this, arguments); + } + + const parentSpan = this[_STORED_PARENT_SPAN]; + const attributes: SpanAttributes = {}; + if (self._config.dbStatementSerializer) { + attributes[SemanticAttributes.DB_STATEMENT] = + self._config.dbStatementSerializer(this.op, { + condition: this._conditions, + updates: this._update, + options: this.options, + fields: this._fields, + }); + } + const span = self._startSpan( + this.mongooseCollection, + this.model.modelName, + this.op, + attributes, + parentSpan + ); + + return self._handleResponse( + span, + originalExec, + this, + arguments, + callback, + moduleVersion + ); + }; + }; + } + + private patchOnModelMethods(op: string, moduleVersion: string | undefined) { + const self = this; + this._diag.debug(`patching mongoose Model '${op}' operation`); + return (originalOnModelFunction: Function) => { + return function method(this: any, options?: any, callback?: Function) { + if ( + self._config.requireParentSpan && + trace.getSpan(context.active()) === undefined + ) { + return originalOnModelFunction.apply(this, arguments); + } + + const serializePayload: SerializerPayload = { document: this }; + if (options && !(options instanceof Function)) { + serializePayload.options = options; + } + const attributes: SpanAttributes = {}; + if (self._config.dbStatementSerializer) { + attributes[SemanticAttributes.DB_STATEMENT] = + self._config.dbStatementSerializer(op, serializePayload); + } + const span = self._startSpan( + this.constructor.collection, + this.constructor.modelName, + op, + attributes + ); + + if (options instanceof Function) { + callback = options; + options = undefined; + } + + return self._handleResponse( + span, + originalOnModelFunction, + this, + arguments, + callback, + moduleVersion + ); + }; + }; + } + + // we want to capture the otel span on the object which is calling exec. + // in the special case of aggregate, we need have no function to path + // on the Aggregate object to capture the context on, so we patch + // the aggregate of Model, and set the context on the Aggregate object + private patchModelAggregate() { + const self = this; + this._diag.debug('patched mongoose model aggregate function'); + return (original: Function) => { + return function captureSpanContext(this: any) { + const currentSpan = trace.getSpan(context.active()); + const aggregate = self._callOriginalFunction(() => + original.apply(this, arguments) + ); + if (aggregate) aggregate[_STORED_PARENT_SPAN] = currentSpan; + return aggregate; + }; + }; + } + + private patchAndCaptureSpanContext(funcName: string) { + const self = this; + this._diag.debug(`patching mongoose query ${funcName} function`); + return (original: Function) => { + return function captureSpanContext(this: any) { + this[_STORED_PARENT_SPAN] = trace.getSpan(context.active()); + return self._callOriginalFunction(() => + original.apply(this, arguments) + ); + }; + }; + } + + private _startSpan( + collection: mongoose.Collection, + modelName: string, + operation: string, + attributes: SpanAttributes, + parentSpan?: Span + ): Span { + return this.tracer.startSpan( + `mongoose.${modelName}.${operation}`, + { + kind: SpanKind.CLIENT, + attributes: { + ...attributes, + ...getAttributesFromCollection(collection), + [SemanticAttributes.DB_OPERATION]: operation, + [SemanticAttributes.DB_SYSTEM]: 'mongoose', + }, + }, + parentSpan ? trace.setSpan(context.active(), parentSpan) : undefined + ); + } + + private _handleResponse( + span: Span, + exec: Function, + originalThis: any, + args: IArguments, + callback?: Function, + moduleVersion: string | undefined = undefined + ) { + const self = this; + if (callback instanceof Function) { + return self._callOriginalFunction(() => + handleCallbackResponse( + callback, + exec, + originalThis, + span, + self._config.responseHook, + moduleVersion + ) + ); + } else { + const response = self._callOriginalFunction(() => + exec.apply(originalThis, args) + ); + return handlePromiseResponse( + response, + span, + self._config.responseHook, + moduleVersion + ); + } + } + + private _callOriginalFunction(originalFunction: (...args: any[]) => T): T { + if (this._config?.suppressInternalInstrumentation) { + return context.with(suppressTracing(context.active()), originalFunction); + } else { + return originalFunction(); + } + } +} diff --git a/plugins/node/instrumentation-mongoose/src/types.ts b/plugins/node/instrumentation-mongoose/src/types.ts new file mode 100644 index 0000000000..8afce8e347 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/src/types.ts @@ -0,0 +1,62 @@ +/* + * 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 { Span } from '@opentelemetry/api'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export interface SerializerPayload { + condition?: any; + options?: any; + updates?: any; + document?: any; + aggregatePipeline?: any; + fields?: any; +} + +export type DbStatementSerializer = ( + operation: string, + payload: SerializerPayload +) => string; + +export interface ResponseInfo { + moduleVersion: string | undefined; + response: any; +} + +export type MongooseResponseCustomAttributesFunction = ( + span: Span, + responseInfo: ResponseInfo +) => void; + +export interface MongooseInstrumentationConfig extends InstrumentationConfig { + /** + * Mongoose operation use mongodb under the hood. + * If mongodb instrumentation is enabled, a mongoose operation will also create + * a mongodb operation describing the communication with mongoDB servers. + * Setting the `suppressInternalInstrumentation` config value to `true` will + * cause the instrumentation to suppress instrumentation of underlying operations, + * effectively causing mongodb spans to be non-recordable. + */ + suppressInternalInstrumentation?: boolean; + + /** Custom serializer function for the db.statement tag */ + dbStatementSerializer?: DbStatementSerializer; + + /** hook for adding custom attributes using the response payload */ + responseHook?: MongooseResponseCustomAttributesFunction; + + /** Set to true if you do not want to collect traces that start with mongoose */ + requireParentSpan?: boolean; +} diff --git a/plugins/node/instrumentation-mongoose/src/utils.ts b/plugins/node/instrumentation-mongoose/src/utils.ts new file mode 100644 index 0000000000..2c44c87dae --- /dev/null +++ b/plugins/node/instrumentation-mongoose/src/utils.ts @@ -0,0 +1,107 @@ +/* + * 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 { SpanAttributes, SpanStatusCode, diag, Span } from '@opentelemetry/api'; +import type { Collection } from 'mongoose'; +import { MongooseResponseCustomAttributesFunction } from './types'; +import { safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +export function getAttributesFromCollection( + collection: Collection +): SpanAttributes { + return { + [SemanticAttributes.DB_MONGODB_COLLECTION]: collection.name, + [SemanticAttributes.DB_NAME]: collection.conn.name, + [SemanticAttributes.DB_USER]: collection.conn.user, + [SemanticAttributes.NET_PEER_NAME]: collection.conn.host, + [SemanticAttributes.NET_PEER_PORT]: collection.conn.port, + }; +} + +function setErrorStatus(span: Span, error: any = {}) { + span.recordException(error); + + span.setStatus({ + code: SpanStatusCode.ERROR, + message: `${error.message} ${ + error.code ? `\nMongoose Error Code: ${error.code}` : '' + }`, + }); +} + +function applyResponseHook( + span: Span, + response: any, + responseHook?: MongooseResponseCustomAttributesFunction, + moduleVersion: string | undefined = undefined +) { + if (!responseHook) { + return; + } + + safeExecuteInTheMiddle( + () => responseHook(span, { moduleVersion, response }), + e => { + if (e) { + diag.error('mongoose instrumentation: responseHook error', e); + } + }, + true + ); +} + +export function handlePromiseResponse( + execResponse: any, + span: Span, + responseHook?: MongooseResponseCustomAttributesFunction, + moduleVersion: string | undefined = undefined +): any { + if (!(execResponse instanceof Promise)) { + applyResponseHook(span, execResponse, responseHook, moduleVersion); + span.end(); + return execResponse; + } + + return execResponse + .then(response => { + applyResponseHook(span, response, responseHook, moduleVersion); + return response; + }) + .catch(err => { + setErrorStatus(span, err); + throw err; + }) + .finally(() => span.end()); +} + +export function handleCallbackResponse( + callback: Function, + exec: Function, + originalThis: any, + span: Span, + responseHook?: MongooseResponseCustomAttributesFunction, + moduleVersion: string | undefined = undefined +) { + return exec.apply(originalThis, [ + (err: Error, response: any) => { + err + ? setErrorStatus(span, err) + : applyResponseHook(span, response, responseHook, moduleVersion); + span.end(); + return callback!(err, response); + }, + ]); +} diff --git a/plugins/node/instrumentation-mongoose/test/asserts.ts b/plugins/node/instrumentation-mongoose/test/asserts.ts new file mode 100644 index 0000000000..f6c7e15ec3 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/asserts.ts @@ -0,0 +1,35 @@ +/* + * 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 * as expect from 'expect'; +import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { SpanStatusCode } from '@opentelemetry/api'; +import { SerializerPayload } from '../src'; +import { DB_NAME, MONGO_HOST, MONGO_PORT } from './config'; + +export const assertSpan = (span: ReadableSpan) => { + expect(span.status.code).toBe(SpanStatusCode.UNSET); + expect(span.attributes[SemanticAttributes.DB_SYSTEM]).toEqual('mongoose'); + expect(span.attributes[SemanticAttributes.DB_MONGODB_COLLECTION]).toEqual( + 'users' + ); + expect(span.attributes[SemanticAttributes.DB_NAME]).toEqual(DB_NAME); + expect(span.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(MONGO_HOST); + expect(span.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(MONGO_PORT); +}; + +export const getStatement = (span: ReadableSpan): SerializerPayload => + JSON.parse(span.attributes[SemanticAttributes.DB_STATEMENT] as string); diff --git a/plugins/node/instrumentation-mongoose/test/config.ts b/plugins/node/instrumentation-mongoose/test/config.ts new file mode 100644 index 0000000000..7e6426d34f --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/config.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ +export const DB_NAME = process.env.MONGODB_DB || 'opentelemetry-tests'; +export const MONGO_HOST = process.env.MONGODB_HOST || 'localhost'; +export const MONGO_PORT = Number(process.env.MONGODB_PORT || 27017); + +export const MONGO_URI = `mongodb://${MONGO_HOST}/${MONGO_PORT}`; diff --git a/plugins/node/instrumentation-mongoose/test/mongoose.test.ts b/plugins/node/instrumentation-mongoose/test/mongoose.test.ts new file mode 100644 index 0000000000..6ba0297bd5 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/mongoose.test.ts @@ -0,0 +1,726 @@ +/* + * 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 * as expect from 'expect'; +import { context, ROOT_CONTEXT } from '@opentelemetry/api'; +import { SemanticAttributes } 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'; + +// Please run mongodb in the background: docker run -d -p 27017:27017 -v ~/data:/data/db mongo +describe('mongoose instrumentation', () => { + 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) { + // 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) => + JSON.stringify(payload), + }); + instrumentation.enable(); + await loadUsers(); + await User.createIndexes(); + }); + + afterEach(async () => { + instrumentation.disable(); + 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[SemanticAttributes.DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.document).toEqual(expect.objectContaining(document)); + }); + + it('instrumenting save operation with 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[SemanticAttributes.DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.document).toEqual(expect.objectContaining(document)); + done(); + }); + }); + + it('instrumenting find operation', async () => { + await User.find({ id: '_test' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('find'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.condition).toEqual({ id: '_test' }); + }); + + it('instrumenting multiple find operations', async () => { + await Promise.all([ + User.find({ id: '_test1' }), + User.find({ id: '_test2' }), + ]); + + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[0] as ReadableSpan); + assertSpan(spans[1] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('find'); + expect(spans[0].attributes[SemanticAttributes.DB_STATEMENT]).toMatch( + /.*{"id":"_test[1-2]"}.*/g + ); + expect(spans[1].attributes[SemanticAttributes.DB_OPERATION]).toBe('find'); + expect(spans[1].attributes[SemanticAttributes.DB_STATEMENT]).toMatch( + /.*{"id":"_test[1-2]"}.*/g + ); + }); + + it('instrumenting find operation with chaining structures', async () => { + await User.find({ id: '_test' }).skip(1).limit(2).sort({ email: 'asc' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('find'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.condition).toEqual({ id: '_test' }); + expect(statement.options).toEqual({ + skip: 1, + limit: 2, + sort: { email: 1 }, + }); + }); + + 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[SemanticAttributes.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[SemanticAttributes.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' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'deleteOne' + ); + }); + + it('instrumenting updateOne operation on models', async () => { + const user = await User.findOne({ email: 'john.doe@example.com' }); + await user!.updateOne({ $inc: { age: 1 } }, { skip: 0 }); + + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[1] as ReadableSpan); + expect(spans[1].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'updateOne' + ); + + const statement = getStatement(spans[1] as ReadableSpan); + expect(statement.options).toEqual({ skip: 0 }); + expect(statement.updates).toEqual({ $inc: { age: 1 } }); + expect(statement.condition._id).toBeDefined(); + }); + + it('instrumenting updateOne operation', async () => { + await User.updateOne( + { email: 'john.doe@example.com' }, + { $inc: { age: 1 } }, + { skip: 0 } + ); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'updateOne' + ); + + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({ skip: 0 }); + expect(statement.updates).toEqual({ $inc: { age: 1 } }); + 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[SemanticAttributes.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' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'countDocuments' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + }); + + it('instrumenting estimatedDocumentCount operation', async () => { + await User.estimatedDocumentCount(); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'estimatedDocumentCount' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({}); + }); + + it('instrumenting deleteMany operation', async () => { + await User.deleteMany(); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'deleteMany' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({}); + }); + + it('instrumenting findOne operation', async () => { + await User.findOne({ email: 'john.doe@example.com' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'findOne' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + 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[SemanticAttributes.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 }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'updateOne' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + expect(statement.updates).toEqual({ age: 55 }); + }); + + it('instrumenting updateMany operation', async () => { + await User.updateMany({ age: 18 }, { isDeleted: true }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'updateMany' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ age: 18 }); + expect(statement.updates).toEqual({ isDeleted: true }); + }); + + it('instrumenting findOneAndDelete operation', async () => { + await User.findOneAndDelete({ email: 'john.doe@example.com' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'findOneAndDelete' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + 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[SemanticAttributes.DB_OPERATION]).toBe( + 'findOne' + ); + expect(spans[1].attributes[SemanticAttributes.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[SemanticAttributes.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', + lastName: 'Doe', + email: 'john.doe+1@example.com', + }; + await User.create(document); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.document).toEqual(expect.objectContaining(document)); + }); + + it('instrumenting aggregate operation', async () => { + await 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[SemanticAttributes.DB_OPERATION]).toBe( + 'aggregate' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.aggregatePipeline).toEqual([ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ]); + }); + + 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[SemanticAttributes.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' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.condition).toEqual({ id: '_test' }); + expect(statement.options).toEqual({ + skip: 1, + limit: 2, + sort: { email: 1 }, + }); + }); + + it('empty dbStatementSerializer does not create a statement attribute', async () => { + instrumentation.disable(); + instrumentation.setConfig({ dbStatementSerializer: undefined }); + instrumentation.enable(); + await User.find({ id: '_test' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_STATEMENT]).toBe( + undefined + ); + }); + + it('projection is sent to serializer', async () => { + instrumentation.disable(); + instrumentation.setConfig({ + dbStatementSerializer: (_operation: string, payload) => + JSON.stringify(payload), + }); + instrumentation.enable(); + + const projection = { firstName: 1 }; + await User.find({ id: '_test1' }, projection); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + const reqPayload = JSON.parse( + spans[0].attributes[SemanticAttributes.DB_STATEMENT] as string + ); + expect(reqPayload.fields).toStrictEqual(projection); + }); + + 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 async/await in exec patch', async () => { + await User.deleteOne({ email: 'john.doe@example.com' }); + 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, + } + ); + }); + + 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', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + const createdUser = await user.save(); + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[RESPONSE]).toEqual( + JSON.stringify(createdUser) + ); + }); + + 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' } }, + { $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 }, + ]); + }); + + 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({ + responseHook: () => { + throw new Error('some error'); + }, + }); + instrumentation.enable(); + await User.deleteOne({ email: 'john.doe@example.com' }); + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[RESPONSE]).toBe(undefined); + }); + }); + + describe('moduleVersion reporting on hook', () => { + const VERSION_ATTR = 'module.version'; + beforeEach(() => { + instrumentation.disable(); + instrumentation.setConfig({ + responseHook: (span, responseInfo) => + span.setAttribute(VERSION_ATTR, responseInfo.moduleVersion!), + }); + instrumentation.enable(); + }); + + it('exec patch', async () => { + await User.deleteOne({ email: 'john.doe@example.com' }); + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[VERSION_ATTR]).toMatch( + /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ + ); + }); + + it('model methods patch', 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[VERSION_ATTR]).toMatch( + /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ + ); + }); + + it('aggregate patch', async () => { + await 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[VERSION_ATTR]).toMatch( + /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ + ); + }); + }); + + describe('requireParentSpan', () => { + beforeEach(() => { + instrumentation.disable(); + instrumentation.setConfig({ + requireParentSpan: true, + }); + instrumentation.enable(); + }); + + it('should not start span on mongoose method', async () => { + await context.with(ROOT_CONTEXT, async () => { + const user: IUser = new User({ + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }); + await user.save(); + }); + + const spans = getTestSpans(); + expect(spans.length).toBe(0); + }); + + it('should not start span on find', async () => { + await context.with(ROOT_CONTEXT, async () => { + await User.find({ id: '_test' }); + }); + + const spans = getTestSpans(); + expect(spans.length).toBe(0); + }); + + it('should not start span on aggregate', async () => { + await context.with(ROOT_CONTEXT, async () => { + await User.aggregate([ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ]); + }); + + const spans = getTestSpans(); + expect(spans.length).toBe(0); + }); + }); +}); diff --git a/plugins/node/instrumentation-mongoose/test/user.ts b/plugins/node/instrumentation-mongoose/test/user.ts new file mode 100644 index 0000000000..0e8eff02eb --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/user.ts @@ -0,0 +1,59 @@ +/* + * 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 { Schema, Document } from 'mongoose'; +import * as mongoose from 'mongoose'; + +export interface IUser extends Document { + email: string; + firstName: string; + lastName: string; + age: number; +} + +const UserSchema: Schema = new Schema({ + email: { type: String, required: true, unique: true }, + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + age: { type: Number, required: false }, +}); + +// Export the model and return your IUser interface +const User = mongoose.model('User', UserSchema); +export default User; + +export const loadUsers = async () => { + await User.insertMany([ + new User({ + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + age: 18, + }), + new User({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@example.com', + age: 19, + }), + new User({ + firstName: 'Michael', + lastName: 'Fox', + email: 'michael.fox@example.com', + age: 16, + }), + ]); + await User.createIndexes(); +}; diff --git a/plugins/node/instrumentation-mongoose/tsconfig.json b/plugins/node/instrumentation-mongoose/tsconfig.json new file mode 100644 index 0000000000..5c3680dd33 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build", + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/release-please-config.json b/release-please-config.json index b3771cad45..9671aba388 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -19,6 +19,7 @@ "packages/opentelemetry-propagation-utils": {}, "packages/opentelemetry-test-utils": {}, "plugins/node/instrumentation-lru-memoizer": {}, + "plugins/node/instrumentation-mongoose": {}, "plugins/node/instrumentation-amqplib": {}, "plugins/node/instrumentation-fs": {}, "plugins/node/instrumentation-tedious": {}, From 1a7f2a0f1c3806738864e6a356c832b46994a6ad Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 29 Aug 2022 10:18:35 +0300 Subject: [PATCH 2/7] chore: markdown lint --- plugins/node/instrumentation-mongoose/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/node/instrumentation-mongoose/README.md b/plugins/node/instrumentation-mongoose/README.md index ebd17e0745..4a42a9ba05 100644 --- a/plugins/node/instrumentation-mongoose/README.md +++ b/plugins/node/instrumentation-mongoose/README.md @@ -40,9 +40,7 @@ registerInstrumentations({ ## Migration From opentelemetry-instrumentation-mongoose -This instrumentation was originally published under the name [`"@wdalmut/opentelemetry-plugin-mongoose"`] in [this repo](https://github.com/wdalmut/opentelemetry-plugin-mongoose). - -It was then ported and maintained under the name `"opentelemetry-instrumentation-mongoose"` in [this repo](https://github.com/aspecto-io/opentelemetry-ext-js). +This instrumentation was originally published and maintained under the name `"opentelemetry-instrumentation-mongoose"` in [this repo](https://github.com/aspecto-io/opentelemetry-ext-js). Few breaking changes were made during porting to the contrib repo to align with conventions: From b51f8bfc04218ab18e15cc3d0c92d3942d5b02a2 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 29 Aug 2022 10:24:59 +0300 Subject: [PATCH 3/7] chore: markdown lint --- plugins/node/instrumentation-mongoose/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-mongoose/README.md b/plugins/node/instrumentation-mongoose/README.md index 4a42a9ba05..22a600e07e 100644 --- a/plugins/node/instrumentation-mongoose/README.md +++ b/plugins/node/instrumentation-mongoose/README.md @@ -40,7 +40,7 @@ registerInstrumentations({ ## Migration From opentelemetry-instrumentation-mongoose -This instrumentation was originally published and maintained under the name `"opentelemetry-instrumentation-mongoose"` in [this repo](https://github.com/aspecto-io/opentelemetry-ext-js). +This instrumentation was originally published and maintained under the name `"opentelemetry-instrumentation-mongoose"` in [this repo](https://github.com/aspecto-io/opentelemetry-ext-js). Few breaking changes were made during porting to the contrib repo to align with conventions: From 52d9213be63d7479350b2fd7b533b943fa8d56f4 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Wed, 7 Sep 2022 18:58:32 +0300 Subject: [PATCH 4/7] chore(mongoose): upgrade to core v0.32.0 --- plugins/node/instrumentation-mongoose/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/node/instrumentation-mongoose/package.json b/plugins/node/instrumentation-mongoose/package.json index 203943dfe8..4fe8840f4b 100644 --- a/plugins/node/instrumentation-mongoose/package.json +++ b/plugins/node/instrumentation-mongoose/package.json @@ -46,7 +46,7 @@ }, "devDependencies": { "@opentelemetry/api": "^1.0.0", - "@opentelemetry/contrib-test-utils": "^0.31.0", + "@opentelemetry/contrib-test-utils": "^0.32.0", "@opentelemetry/sdk-trace-base": "^1.3.1", "@types/mocha": "8.2.3", "@types/node": "16.11.21", @@ -62,7 +62,7 @@ }, "dependencies": { "@opentelemetry/core": "^1.0.0", - "@opentelemetry/instrumentation": "^0.31.0", + "@opentelemetry/instrumentation": "^0.32.0", "@opentelemetry/semantic-conventions": "^1.0.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-mongoose#readme" From da280983496b6cf361d755861e73583931d96881 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Tue, 13 Sep 2022 16:45:20 +0300 Subject: [PATCH 5/7] fix(mongoose): pin mongoose version to avoid breaking types file --- plugins/node/instrumentation-mongoose/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-mongoose/package.json b/plugins/node/instrumentation-mongoose/package.json index 4fe8840f4b..a7521e2d57 100644 --- a/plugins/node/instrumentation-mongoose/package.json +++ b/plugins/node/instrumentation-mongoose/package.json @@ -53,7 +53,7 @@ "expect": "27.4.2", "gts": "3.1.0", "mocha": "7.2.0", - "mongoose": "^6.5.3", + "mongoose": "6.5.2", "nyc": "15.1.0", "rimraf": "3.0.2", "test-all-versions": "5.0.1", From 1855c14bc3c5a54abeceb16142ca084b3bda96d3 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Tue, 13 Sep 2022 17:50:03 +0300 Subject: [PATCH 6/7] ci(mongoose): don't run test on node 8 and 10 --- .github/workflows/unit-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 2b6bdd74f6..7cb33b1936 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -19,12 +19,14 @@ jobs: --ignore @opentelemetry/instrumentation-amqplib --ignore @opentelemetry/instrumentation-redis-4 --ignore @opentelemetry/instrumentation-hapi + --ignore @opentelemetry/instrumentation-mongoose - node: "10" lerna-extra-args: >- --ignore @opentelemetry/instrumentation-aws-sdk --ignore @opentelemetry/instrumentation-pino --ignore @opentelemetry/instrumentation-redis-4 --ignore @opentelemetry/instrumentation-hapi + --ignore @opentelemetry/instrumentation-mongoose - node: "12" lerna-extra-args: >- --ignore @opentelemetry/instrumentation-redis-4 From bf2fec75f84fe13edeff623aa7e6dfc4ba56be5a Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Thu, 22 Sep 2022 12:18:08 +0300 Subject: [PATCH 7/7] chore(mongoose): minimum node version 14 --- plugins/node/instrumentation-mongoose/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-mongoose/package.json b/plugins/node/instrumentation-mongoose/package.json index a7521e2d57..db0b06f90d 100644 --- a/plugins/node/instrumentation-mongoose/package.json +++ b/plugins/node/instrumentation-mongoose/package.json @@ -31,7 +31,7 @@ "author": "OpenTelemetry Authors", "license": "Apache-2.0", "engines": { - "node": ">=8.12.0" + "node": ">=14.0" }, "files": [ "build/src/**/*.js",