diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 14b6242f6cd..17dd798e246 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -1,19 +1,26 @@ components: + detectors/node/opentelemetry-resource-detector-aws: + - NathanielRN + - willarmiros + detectors/node/opentelemetry-resource-detector-alibaba-cloud: + - legendecas packages/opentelemetry-id-generator-aws-xray: - NathanielRN - willarmiros plugins/node/opentelemetry-instrumentation-aws-lambda: - NathanielRN - willarmiros - plugins/node/opentelemetry-instrumentation-restify: + plugins/node/opentelemetry-instrumentation-generic-pool: - rauno56 - plugins/node/opentelemetry-instrumentation-router: + plugins/node/opentelemetry-instrumentation-knex: - rauno56 plugins/node/opentelemetry-instrumentation-memcached: - rauno56 - plugins/node/opentelemetry-instrumentation-knex: + plugins/node/opentelemetry-instrumentation-nestjs-core: - rauno56 - plugins/node/opentelemetry-instrumentation-generic-pool: + plugins/node/opentelemetry-instrumentation-restify: + - rauno56 + plugins/node/opentelemetry-instrumentation-router: - rauno56 plugins/node/opentelemetry-instrumentation-ioredis: - blumamir @@ -23,11 +30,6 @@ components: propagators/opentelemetry-propagator-aws-xray: - NathanielRN - willarmiros - detectors/node/opentelemetry-resource-detector-aws: - - NathanielRN - - willarmiros - detectors/node/opentelemetry-resource-detector-alibaba-cloud: - - legendecas ignored-authors: - renovate-bot diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/.eslintignore b/plugins/node/opentelemetry-instrumentation-nestjs-core/.eslintignore new file mode 100644 index 00000000000..378eac25d31 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/.eslintignore @@ -0,0 +1 @@ +build diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/.eslintrc.js b/plugins/node/opentelemetry-instrumentation-nestjs-core/.eslintrc.js new file mode 100644 index 00000000000..5054f4d2878 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + env: { + mocha: true, + node: true + }, + ...require('../../../eslint.config.js') +}; diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/.npmignore b/plugins/node/opentelemetry-instrumentation-nestjs-core/.npmignore new file mode 100644 index 00000000000..9505ba9450f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/LICENSE b/plugins/node/opentelemetry-instrumentation-nestjs-core/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/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 [yyyy] [name of copyright owner] + + 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/opentelemetry-instrumentation-nestjs-core/README.md b/plugins/node/opentelemetry-instrumentation-nestjs-core/README.md new file mode 100644 index 00000000000..29f8b7acb26 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/README.md @@ -0,0 +1,86 @@ +# OpenTelemetry NestJS Instrumentation for Node.js + +[![NPM Published Version][npm-img]][npm-url] +[![dependencies][dependencies-image]][dependencies-url] +[![devDependencies][devDependencies-image]][devDependencies-url] +[![Apache License][license-image]][license-image] + +This module provides automatic tracing instrumentation for [Nest framework][pkg-web-url]. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-nestjs-core +``` + +### Supported Versions + +- `>=4.0.0` + +## Usage + +OpenTelemetry Nest Instrumentation allows the user to automatically collect trace data from the controller handlers and export them to the backend of choice. + +To load a specific instrumentation (**Nest** in this case), specify it in the registerInstrumentations' configuration. + +```javascript +const { NodeTracerProvider } = require('@opentelemetry/node'); +const { NestInstrumentation } = require('@opentelemetry/instrumentation-nestjs-core'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + new NestInstrumentation(), + ], +}); +``` + +## Emitted Spans + +| Name | `nestjs.type` | Description | Included attributes +| ---- | ---- | ---- | ---- +`Create Nest App` | `app_creation` | Traces the bootup for the Nest App. The `NestFactory(Static).create` call. | `nestjs.module` +`.` | `request_context` | Traces the whole request context. | `http.*`, `nestjs.callback` +`` | `handler` | Traces the work of a specific controller member function. | `nestjs.callback` + +#### Attributes + +| Name | Description +| ---- | ---- +| `component`* | "@nestjs/core" +| `nestjs.version`* | Version of instrumented `@nestjs/core` package +| `nestjs.type`* | See [NestType](./src/enums/NestType.ts) +| `nestjs.module` | Nest module class name +| `nestjs.controller` | Nest controller class name +| `nestjs.callback` | The function name of the member in the controller +| `http.method` | HTTP method +| `http.url` | Full request URL +| `http.route` | Route assigned to handler. Ex: `/users/:id` + +\* included in all of the spans. + +## 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 +[dependencies-image]: https://status.david-dm.org/gh/open-telemetry/opentelemetry-js-contrib.svg?path=plugins%2Fnode%2Fopentelemetry-instrumentation-nestjs-core +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=plugins%2Fnode%2Fopentelemetry-instrumentation-nestjs-core +[devDependencies-image]: https://status.david-dm.org/gh/open-telemetry/opentelemetry-js-contrib.svg?path=plugins%2Fnode%2Fopentelemetry-instrumentation-nestjs-core&type=dev +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=plugins%2Fnode%2Fopentelemetry-instrumentation-nestjs-core&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-nestjs-core +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-nestjs-core.svg +[pkg-repo-url]: https://github.com/nestjs/nest +[pkg-npm-url]: https://www.npmjs.com/package/@nestjs/core +[pkg-web-url]: https://nestjs.com/ diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/package.json b/plugins/node/opentelemetry-instrumentation-nestjs-core/package.json new file mode 100644 index 00000000000..15f6c98736e --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/package.json @@ -0,0 +1,79 @@ +{ + "name": "@opentelemetry/instrumentation-nestjs-core", + "version": "0.24.0", + "description": "OpenTelemetry NestJS automatic instrumentation package.", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js-contrib", + "scripts": { + "clean": "rimraf build/*", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "compile": "npm run version:update && tsc -p .", + "compile:watch": "tsc -w", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "precompile": "tsc --version", + "prepare": "npm run compile", + "tdd": "npm run test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "version:update": "node ../../../scripts/version-update.js" + }, + "keywords": [ + "opentelemetry", + "nestjs", + "nodejs", + "tracing", + "profiling", + "instrumentation" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.5.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.1" + }, + "devDependencies": { + "@nestjs/common": "^8.0.5", + "@nestjs/core": "^8.0.5", + "@nestjs/microservices": "^8.0.5", + "@nestjs/platform-express": "^8.0.5", + "@nestjs/websockets": "^8.0.5", + "@opentelemetry/api": "1.0.1", + "@opentelemetry/context-async-hooks": "0.24.0", + "@opentelemetry/node": "0.24.0", + "@opentelemetry/tracing": "0.24.0", + "@types/mocha": "7.0.2", + "@types/node": "14.17.6", + "@types/semver": "^7.3.8", + "@types/vinyl-fs": "^2.4.12", + "codecov": "3.8.3", + "cross-env": "7.0.3", + "gts": "3.1.0", + "mocha": "7.2.0", + "nyc": "15.1.0", + "reflect-metadata": "^0.1.13", + "rimraf": "3.0.2", + "rxjs": "^7.3.0", + "rxjs-compat": "^6.6.7", + "semver": "^7.3.5", + "ts-mocha": "8.0.0", + "typescript": "4.3.5" + }, + "dependencies": { + "@opentelemetry/instrumentation": "^0.24.0", + "@opentelemetry/semantic-conventions": "^0.24.0" + } +} diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/scripts/test-all-versions b/plugins/node/opentelemetry-instrumentation-nestjs-core/scripts/test-all-versions new file mode 100755 index 00000000000..07de70e0706 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/scripts/test-all-versions @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +# return to the default package.json and install all deps +git checkout -- package.json +npm install + +test_silent () { + npm run test &> /dev/null && echo OK || echo FAIL +} + +echo -e "\n## 4.0.0" +npm install -D \ + reflect-metadata@0.1.13 \ + @nestjs/common@^4.0.0 \ + @nestjs/core@^4.0.0 \ + @nestjs/microservices@^4.0.0 \ + @nestjs/websockets@^4.0.0 \ + rxjs@^5.4.2 \ + rxjs-compat@^6.0.0 > /dev/null +test_silent + +echo -e "\n## ^4.0.0" +npm install -D \ + reflect-metadata@0.1.13 \ + @nestjs/common@^4.4.0 \ + @nestjs/core@^4.4.0 \ + @nestjs/microservices@^4.4.0 \ + @nestjs/websockets@^4.4.0 \ + rxjs@^5.4.2 \ + rxjs-compat@^6.0.0 > /dev/null +test_silent + +echo -e "\n## ^5.0.0" +npm install -D \ + reflect-metadata@0.1.13 \ + @nestjs/common@^5.0.0 \ + @nestjs/core@^5.0.0 \ + @nestjs/microservices@^5.0.0 \ + @nestjs/websockets@^5.0.0 \ + rxjs@^6.0.0 \ + rxjs-compat@^6.0.0 > /dev/null +test_silent + +echo -e "\n## ^6.0.0" +npm install -D \ + reflect-metadata@0.1.13 \ + @nestjs/common@^6.0.0 \ + @nestjs/core@^6.0.0 \ + @nestjs/microservices@^6.0.0 \ + @nestjs/websockets@^6.0.0 \ + @nestjs/platform-express@^6.0.0 \ + rxjs@^6.0.0 \ + rxjs-compat@^6.0.0 > /dev/null +test_silent + +echo -e "\n## ^7.0.0" +npm install -D \ + reflect-metadata@0.1.13 \ + @nestjs/common@^7.0.0 \ + @nestjs/core@^7.0.0 \ + @nestjs/microservices@^7.0.0 \ + @nestjs/websockets@^7.0.0 \ + @nestjs/platform-express@^7.0.0 \ + rxjs@^7.0.0 \ + rxjs-compat@latest > /dev/null +test_silent + +echo -e "\n## ^8.0.0" +npm install -D \ + reflect-metadata@0.1.13 \ + @nestjs/common@^8.0.0 \ + @nestjs/core@^8.0.0 \ + @nestjs/microservices@^8.0.0 \ + @nestjs/websockets@^8.0.0 \ + @nestjs/platform-express@^8.0.0 \ + rxjs@^7.2.0 \ + rxjs-compat@latest > /dev/null +test_silent diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/src/enums/AttributeNames.ts b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/enums/AttributeNames.ts new file mode 100644 index 00000000000..da84db8f255 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/enums/AttributeNames.ts @@ -0,0 +1,26 @@ +/* + * 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 enum AttributeNames { + VERSION = 'nestjs.version', + TYPE = 'nestjs.type', + MODULE = 'nestjs.module', + CONTROLLER = 'nestjs.controller', + CALLBACK = 'nestjs.callback', + PIPES = 'nestjs.pipes', + INTERCEPTORS = 'nestjs.interceptors', + GUARDS = 'nestjs.guards', +} diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/src/enums/NestType.ts b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/enums/NestType.ts new file mode 100644 index 00000000000..69e6fa60b85 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/enums/NestType.ts @@ -0,0 +1,21 @@ +/* + * 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 enum NestType { + APP_CREATION = 'app_creation', + REQUEST_CONTEXT = 'request_context', + REQUEST_HANDLER = 'handler', +} diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/src/enums/index.ts b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/enums/index.ts new file mode 100644 index 00000000000..83f7d0769d0 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/enums/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { AttributeNames } from './AttributeNames'; +export { NestType } from './NestType'; diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/src/index.ts b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/index.ts new file mode 100644 index 00000000000..d7649869f9f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/index.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. + */ + +import { Instrumentation } from './instrumentation'; + +export * from './instrumentation'; +export { Instrumentation as NestInstrumentation }; diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/instrumentation.ts new file mode 100644 index 00000000000..c94c2ec0a3f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/instrumentation.ts @@ -0,0 +1,249 @@ +/* + * 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 api from '@opentelemetry/api'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, + isWrapped, +} from '@opentelemetry/instrumentation'; +import type * as NestJS from '@nestjs/core'; +import type { NestFactory } from '@nestjs/core/nest-factory.js'; +import type { RouterExecutionContext } from '@nestjs/core/router/router-execution-context.js'; +import type { Controller } from '@nestjs/common/interfaces'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { VERSION } from './version'; +import { AttributeNames, NestType } from './enums'; + +export class Instrumentation extends InstrumentationBase { + static readonly COMPONENT = '@nestjs/core'; + static readonly COMMON_ATTRIBUTES = { + component: Instrumentation.COMPONENT, + }; + + constructor() { + super('@opentelemetry/instrumentation-nestjs-core', VERSION); + } + + init() { + const module = new InstrumentationNodeModuleDefinition( + Instrumentation.COMPONENT, + ['>=4.0.0'], + (moduleExports, moduleVersion) => { + this._diag.debug( + `Patching ${Instrumentation.COMPONENT}@${moduleVersion}` + ); + return moduleExports; + }, + (moduleExports, moduleVersion) => { + this._diag.debug( + `Unpatching ${Instrumentation.COMPONENT}@${moduleVersion}` + ); + if (moduleExports === undefined) return; + } + ); + + module.files.push( + this.getNestFactoryFileInstrumentation(['>=4.0.0']), + this.getRouterExecutionContextFileInstrumentation(['>=4.0.0']) + ); + + return module; + } + + getNestFactoryFileInstrumentation(versions: string[]) { + return new InstrumentationNodeModuleFile( + '@nestjs/core/nest-factory.js', + versions, + (NestFactoryStatic: any, moduleVersion?: string) => { + this.ensureWrapped( + moduleVersion, + NestFactoryStatic.NestFactoryStatic.prototype, + 'create', + createWrapNestFactoryCreate(this.tracer, moduleVersion) + ); + return NestFactoryStatic; + }, + (NestFactoryStatic: any) => { + this._unwrap(NestFactoryStatic.NestFactoryStatic.prototype, 'create'); + } + ); + } + + getRouterExecutionContextFileInstrumentation(versions: string[]) { + return new InstrumentationNodeModuleFile( + '@nestjs/core/router/router-execution-context.js', + versions, + (RouterExecutionContext: any, moduleVersion?: string) => { + this.ensureWrapped( + moduleVersion, + RouterExecutionContext.RouterExecutionContext.prototype, + 'create', + createWrapCreateHandler(this.tracer, moduleVersion) + ); + return RouterExecutionContext; + }, + (RouterExecutionContext: any) => { + this._unwrap( + RouterExecutionContext.RouterExecutionContext.prototype, + 'create' + ); + } + ); + } + + private ensureWrapped( + moduleVersion: string | undefined, + obj: any, + methodName: string, + wrapper: (original: any) => any + ) { + this._diag.debug( + `Applying ${methodName} patch for ${Instrumentation.COMPONENT}@${moduleVersion}` + ); + if (isWrapped(obj[methodName])) { + this._unwrap(obj, methodName); + } + this._wrap(obj, methodName, wrapper); + } +} + +function createWrapNestFactoryCreate( + tracer: api.Tracer, + moduleVersion?: string +) { + return function wrapCreate(original: typeof NestFactory.create) { + return function createWithTrace( + this: typeof NestFactory, + nestModule: any + /* serverOrOptions */ + ) { + const span = tracer.startSpan('Create Nest App', { + attributes: { + ...Instrumentation.COMMON_ATTRIBUTES, + [AttributeNames.TYPE]: NestType.APP_CREATION, + [AttributeNames.VERSION]: moduleVersion, + [AttributeNames.MODULE]: nestModule.name, + }, + }); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context.with(spanContext, async () => { + try { + return await original.apply(this, arguments as any); + } catch (e) { + throw addError(span, e); + } finally { + span.end(); + } + }); + }; + }; +} + +function createWrapCreateHandler(tracer: api.Tracer, moduleVersion?: string) { + return function wrapCreateHandler( + original: RouterExecutionContext['create'] + ) { + return function createHandlerWithTrace( + this: RouterExecutionContext, + instance: Controller, + callback: (...args: any[]) => unknown + ) { + arguments[1] = createWrapHandler(tracer, moduleVersion, callback); + const handler = original.apply(this, arguments as any); + return function ( + this: any, + req: any, + res: any, + next: (...args: any[]) => unknown + ) { + const callbackName = callback.name; + const instanceName = + instance.constructor && instance.constructor.name + ? instance.constructor.name + : 'UnnamedInstance'; + const spanName = callbackName + ? `${instanceName}.${callbackName}` + : instanceName; + + const span = tracer.startSpan(spanName, { + attributes: { + ...Instrumentation.COMMON_ATTRIBUTES, + [AttributeNames.VERSION]: moduleVersion, + [AttributeNames.TYPE]: NestType.REQUEST_CONTEXT, + [SemanticAttributes.HTTP_METHOD]: req.method, + [SemanticAttributes.HTTP_URL]: req.originalUrl, + [SemanticAttributes.HTTP_ROUTE]: req.route.path, + [AttributeNames.CONTROLLER]: instanceName, + [AttributeNames.CALLBACK]: callbackName, + }, + }); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context.with(spanContext, async () => { + try { + return await handler.apply(this, arguments as any); + } catch (e) { + throw addError(span, e); + } finally { + span.end(); + } + }); + }; + }; + }; +} + +function createWrapHandler( + tracer: api.Tracer, + moduleVersion: string | undefined, + handler: Function +) { + const wrappedHandler = function (this: RouterExecutionContext) { + const span = tracer.startSpan(handler.name || 'anonymous nest handler', { + attributes: { + ...Instrumentation.COMMON_ATTRIBUTES, + [AttributeNames.VERSION]: moduleVersion, + [AttributeNames.TYPE]: NestType.REQUEST_HANDLER, + [AttributeNames.CALLBACK]: handler.name, + }, + }); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context.with(spanContext, async () => { + try { + return await handler.apply(this, arguments); + } catch (e) { + throw addError(span, e); + } finally { + span.end(); + } + }); + }; + + if (handler.name) { + Object.defineProperty(wrappedHandler, 'name', { value: handler.name }); + } + return wrappedHandler; +} + +const addError = (span: api.Span, error: Error) => { + span.recordException(error); + span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message }); + return error; +}; diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/src/version.ts b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/version.ts new file mode 100644 index 00000000000..2902e7d2623 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/src/version.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.24.0'; diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/test/index.test.ts b/plugins/node/opentelemetry-instrumentation-nestjs-core/test/index.test.ts new file mode 100644 index 00000000000..d382d1753c1 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/test/index.test.ts @@ -0,0 +1,241 @@ +/* + * 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 semver from 'semver'; + +import { context, SpanStatusCode } from '@opentelemetry/api'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import * as assert from 'assert'; +import { NestInstrumentation } from '../src'; +import { getRequester, setup, App } from './setup'; + +import * as util from 'util'; + +const LIB_VERSION = require('@nestjs/core/package.json').version; + +const instrumentation = new NestInstrumentation(); +const memoryExporter = new InMemorySpanExporter(); + +util.inspect.defaultOptions.depth = 3; +util.inspect.defaultOptions.breakLength = 200; + +describe('nestjs-core', () => { + const provider = new NodeTracerProvider(); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + instrumentation.setTracerProvider(provider); + let contextManager: AsyncHooksContextManager; + let app: App; + let request = async (path: string): Promise => { + throw new Error('Not yet initialized.'); + }; + + beforeEach(async () => { + contextManager = new AsyncHooksContextManager(); + context.setGlobalContextManager(contextManager.enable()); + instrumentation.setConfig({}); + instrumentation.enable(); + + app = await setup(LIB_VERSION); + request = getRequester(app); + }); + + afterEach(async () => { + await app.close(); + + memoryExporter.reset(); + context.disable(); + instrumentation.disable(); + }); + + it('should capture setup', async () => { + assertSpans(memoryExporter.getFinishedSpans(), [ + { + type: 'app_creation', + service: 'test', + name: 'Create Nest App', + module: 'AppModule', + }, + ]); + }); + + it('should capture requests', async () => { + const path = semver.intersects(LIB_VERSION, '<5.0.0') ? '/' : '/users'; + const url = '/users'; + const instance = 'UsersController'; + const callback = 'getUsers'; + + assert.strictEqual(await request('/users'), 'Hello, world!\n'); + + assertSpans(memoryExporter.getFinishedSpans(), [ + { + type: 'app_creation', + service: 'test', + name: 'Create Nest App', + module: 'AppModule', + }, + { + type: 'handler', + service: 'test', + name: callback, + callback, + parentSpanName: `${instance}.${callback}`, + }, + { + type: 'request_context', + service: 'test', + name: `${instance}.${callback}`, + method: 'GET', + url, + path, + callback, + }, + ]); + }); + + it('should capture errors', async () => { + const path = semver.intersects(LIB_VERSION, '<5.0.0') ? '/' : '/errors'; + const url = '/errors'; + const instance = 'ErrorController'; + const callback = 'getError'; + + assert.strictEqual( + await request('/errors'), + '{"statusCode":500,"message":"Internal server error"}' + ); + + assertSpans(memoryExporter.getFinishedSpans(), [ + { + type: 'app_creation', + service: 'test', + name: 'Create Nest App', + module: 'AppModule', + }, + { + type: 'handler', + service: 'test', + name: callback, + callback, + status: { + code: SpanStatusCode.ERROR, + message: 'custom error', + }, + parentSpanName: `${instance}.${callback}`, + }, + { + type: 'request_context', + service: 'test', + name: `${instance}.${callback}`, + method: 'GET', + url, + path, + callback, + status: { + code: SpanStatusCode.ERROR, + message: 'custom error', + }, + }, + ]); + }); +}); + +const assertSpans = (actualSpans: any[], expectedSpans: any[]) => { + assert(Array.isArray(actualSpans), 'Expected `actualSpans` to be an array'); + assert( + Array.isArray(expectedSpans), + 'Expected `expectedSpans` to be an array' + ); + assert.strictEqual( + actualSpans.length, + expectedSpans.length, + 'Expected span count different from actual' + ); + + actualSpans.forEach((span, idx) => { + const expected = expectedSpans[idx]; + if (expected === null) return; + try { + assert.notStrictEqual(span, undefined); + assert.notStrictEqual(expected, undefined); + + assert.strictEqual(span.attributes.component, '@nestjs/core'); + assert.strictEqual(span.attributes['nestjs.module'], expected.module); + + assert.strictEqual(span.name, expected.name); + + assert.strictEqual(span.attributes['http.method'], expected.method); + assert.strictEqual(span.attributes['http.url'], expected.url); + assert.strictEqual(span.attributes['http.route'], expected.path); + assert.strictEqual(span.attributes['nestjs.type'], expected.type); + assert.strictEqual(span.attributes['nestjs.callback'], expected.callback); + assert.strictEqual( + span.attributes['nest.controller.instance'], + expected.instance + ); + + assert.strictEqual( + span.attributes.component, + NestInstrumentation.COMPONENT + ); + assert.strictEqual( + typeof span.attributes['nestjs.version'], + 'string', + 'nestjs.version not specified' + ); + assert.deepEqual( + span.status, + expected.status || { code: SpanStatusCode.UNSET } + ); + if (typeof expected.parentSpanIdx === 'number') { + assert.strictEqual( + span.parentSpanId, + actualSpans[expected.parentSpanIdx].spanContext().spanId + ); + } else if (typeof expected.parentSpanName === 'string') { + const parentSpan = actualSpans.find( + s => s.name === expected.parentSpanName + ); + assert.notStrictEqual( + parentSpan, + undefined, + `Cannot find span named ${expected.parentSpanName} expected to be the parent of ${span.name}` + ); + assert.strictEqual( + span.parentSpanId, + parentSpan.spanContext().spanId, + `Expected "${expected.parentSpanName}" to be the parent of "${ + span.name + }", but found "${ + actualSpans.find(s => s.spanContext().spanId === span.parentSpanId) + .name + }"` + ); + } else if (expected.parentSpan !== null) { + assert.strictEqual( + span.parentSpanId, + expected.parentSpan?.spanContext().spanId + ); + } + } catch (e) { + e.message = `At span[${idx}] "${span.name}": ${e.message}`; + throw e; + } + }); +}; diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/test/setup.ts b/plugins/node/opentelemetry-instrumentation-nestjs-core/test/setup.ts new file mode 100644 index 00000000000..c4ffc0e2809 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/test/setup.ts @@ -0,0 +1,235 @@ +/* + * 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 http from 'http'; +import * as semver from 'semver'; +import { AddressInfo } from 'net'; +import { + CanActivate, + ExecutionContext, + NestInterceptor, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +// mimics the support for @decorators +const __decorate = function ( + decorators: any, + target?: any, + key?: any, + desc?: any +) { + if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') { + return Reflect.decorate(decorators, target, key, desc); + } + switch (arguments.length) { + case 2: + return decorators.reduceRight((o: any, d: Function) => { + return (d && d(o)) || o; + }, target); + case 3: + return decorators.reduceRight((o: any, d: Function) => { + return (d && d(target, key)) || o; + }, void 0); + case 4: + return decorators.reduceRight((o: any, d: Function) => { + return (d && d(target, key, o)) || o; + }, desc); + } +}; + +const decorateProperty = (obj: any, propName: string, decorators: any) => { + Object.defineProperty( + obj, + propName, + __decorate( + Array.isArray(decorators) ? decorators : [decorators], + obj, + propName, + Object.getOwnPropertyDescriptor(obj, propName) + ) + ); +}; + +const makeModule = ( + name = 'Unnamed', + handler = (...args: unknown[]) => {}, + controllerDecorators: any[] = [] +) => { + const common = require('@nestjs/common'); + const methodName = `get${name}`; + + let Controller = class { + [methodName](...args: any[]) { + return handler(...args); + } + }; + Object.defineProperty(Controller, 'name', { value: `${name}Controller` }); + + Controller = __decorate(controllerDecorators, Controller); + decorateProperty(Controller.prototype, methodName, [common.Get()]); + + let Module = class {}; + Object.defineProperty(Module, 'name', { value: `${name}Module` }); + + Module = __decorate( + [ + common.Module({ + controllers: [Controller], + }), + ], + Module + ); + + return [Controller, Module]; +}; + +export type App = { + close: Function; +}; + +export const setup = async (version: string): Promise => { + const core = require('@nestjs/core'); + const common = require('@nestjs/common'); + + let AppModule = class AppModule {}; + let MyGuard = class MyGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + return true; + } + }; + MyGuard = __decorate( + [ + semver.intersects(version, '^4.0.0') + ? common.Guard() + : common.Injectable(), + ], + MyGuard + ); + let YellInterceptor = class YellInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next + .handle() + .pipe( + map(value => + typeof value === 'string' ? value.toUpperCase() : value + ) + ); + } + }; + YellInterceptor = __decorate( + [ + semver.intersects(version, '^4.0.0') + ? common.Interceptor() + : common.Injectable(), + ], + YellInterceptor + ); + + const [UsersController, UsersModule] = makeModule( + 'Users', + () => 'Hello, world!\n', + [common.Controller('users')] + ); + const [GuardedController, GuardedModule] = makeModule( + 'Guarded', + () => 'Hello, guarded!\n', + [ + common.Controller('guarded'), + common.UseGuards(MyGuard), + common.UseGuards(MyGuard), + ] + ); + const [InterceptedController, InterceptedModule] = makeModule( + 'Intercepted', + () => 'Hello, Intercepted!\n', + [ + common.Controller('intercepted'), + common.UseInterceptors(YellInterceptor), + common.UseInterceptors(YellInterceptor), + ] + ); + const [ErrorController, ErrorModule] = makeModule( + 'Error', + () => { + throw new Error('custom error'); + }, + [common.Controller('errors')] + ); + + if (semver.intersects(version, '>=4.6.3')) { + AppModule = __decorate( + [ + common.Module({ + imports: [UsersModule, ErrorModule, GuardedModule, InterceptedModule], + controllers: [ + UsersController, + ErrorController, + GuardedController, + InterceptedController, + ], + }), + ], + AppModule + ); + } else { + AppModule = __decorate( + [ + common.Module({ + modules: [UsersModule, ErrorModule, GuardedModule, InterceptedModule], + controllers: [ + UsersController, + ErrorController, + GuardedController, + InterceptedController, + ], + }), + ], + AppModule + ); + } + + const app = await core.NestFactory.create(AppModule); + if (app.listenAsync) { + await app.listenAsync(0, 'localhost'); + } else { + await app.listen(0, 'localhost'); + } + + return app as App; +}; + +export const getRequester = (app: any) => { + const port = (app.httpServer.address() as AddressInfo).port; + return (path: string) => { + return new Promise((resolve, reject) => { + return http.get(`http://localhost:${port}${path}`, resp => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + resolve(data); + }); + resp.on('error', err => { + reject(err); + }); + }); + }); + }; +}; + +export default setup; diff --git a/plugins/node/opentelemetry-instrumentation-nestjs-core/tsconfig.json b/plugins/node/opentelemetry-instrumentation-nestjs-core/tsconfig.json new file mode 100644 index 00000000000..28be80d266c --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-nestjs-core/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +}