diff --git a/examples/connect/README.md b/examples/connect/README.md new file mode 100644 index 0000000000..290dc0e16c --- /dev/null +++ b/examples/connect/README.md @@ -0,0 +1,56 @@ +# Overview + +OpenTelemetry Connect Instrumentation allows the user to automatically collect trace data and export them to the backend of choice (Collector Exporter), to give observability to distributed systems. + +This is a simple example that demonstrates tracing calls made to Connect API. The example shows key aspects of tracing such as +- Root Span (on Client) +- Child Span (on Client) +- Span Events +- Span Attributes + +## Installation + +```sh +$ # from this directory +$ npm install +``` + +## Run the Application + +### Collector - docker container + + - Run docker container with collector + + ```sh + # from this directory + $ npm run docker:start + ``` + +### Server + + - Run the server + + ```sh + # from this directory + $ npm run server + ``` + + - Run the client + + ```sh + # from this directory + npm run client + ``` + +#### Zipkin UI +Go to Zipkin with your browser [http://localhost:9411/]() + +

+ +## Useful links +- For more information on OpenTelemetry, visit: +- For more information on OpenTelemetry for Node.js, visit: + +## LICENSE + +Apache License 2.0 diff --git a/examples/connect/client.js b/examples/connect/client.js new file mode 100644 index 0000000000..dc34f77e6c --- /dev/null +++ b/examples/connect/client.js @@ -0,0 +1,31 @@ +'use strict'; + +// eslint-disable-next-line import/order +const tracing = require('./tracing')('example-connect-client'); +const tracer = tracing.tracer; +const api = require('@opentelemetry/api'); +const axios = require('axios').default; + +function makeRequest() { + tracing.log('starting'); + const span = tracer.startSpan('client.makeRequest()', { + kind: api.SpanKind.CLIENT, + }); + + api.context.with(api.trace.setSpan(api.ROOT_CONTEXT, span), async () => { + try { + const res = await axios.post('http://localhost:8080/run_test'); + tracing.log('status:', res.statusText); + span.setStatus({ code: api.SpanStatusCode.OK }); + } catch (e) { + tracing.log('failed:', e.message); + span.setStatus({ code: api.SpanStatusCode.ERROR, message: e.message }); + } + span.end(); + tracing.log('forcing spans to be exported'); + await tracing.provider.shutdown(); + tracing.log('all spans exported successfully.'); + }); +} + +makeRequest(); diff --git a/examples/connect/docker/collector-config.yaml b/examples/connect/docker/collector-config.yaml new file mode 100644 index 0000000000..04d65a6ba2 --- /dev/null +++ b/examples/connect/docker/collector-config.yaml @@ -0,0 +1,28 @@ +receivers: + otlp: + protocols: + grpc: + http: + cors_allowed_origins: + - http://* + - https://* + +exporters: + zipkin: + endpoint: "http://zipkin-all-in-one:9411/api/v2/spans" + prometheus: + endpoint: "0.0.0.0:9464" + +processors: + batch: + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [zipkin] + processors: [batch] + metrics: + receivers: [otlp] + exporters: [prometheus] + processors: [batch] diff --git a/examples/connect/docker/docker-compose.yaml b/examples/connect/docker/docker-compose.yaml new file mode 100644 index 0000000000..396ca636ec --- /dev/null +++ b/examples/connect/docker/docker-compose.yaml @@ -0,0 +1,21 @@ +version: "3" +services: + # Collector + collector: + image: otel/opentelemetry-collector:0.30.0 +# image: otel/opentelemetry-collector:latest + command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] + volumes: + - ./collector-config.yaml:/conf/collector-config.yaml + ports: + - "9464:9464" + - "4317:4317" + - "55681:55681" + depends_on: + - zipkin-all-in-one + + # Zipkin + zipkin-all-in-one: + image: openzipkin/zipkin:latest + ports: + - "9411:9411" diff --git a/examples/connect/images/trace1.png b/examples/connect/images/trace1.png new file mode 100644 index 0000000000..89e5cf7761 Binary files /dev/null and b/examples/connect/images/trace1.png differ diff --git a/examples/connect/package.json b/examples/connect/package.json new file mode 100644 index 0000000000..b8fd33cfda --- /dev/null +++ b/examples/connect/package.json @@ -0,0 +1,56 @@ +{ + "name": "express-connect", + "private": true, + "version": "0.23.0", + "description": "Example of Connect integration with OpenTelemetry", + "main": "index.js", + "scripts": { + "client": "node ./client.js", + "docker:start": "cd ./docker && docker-compose down && docker-compose up", + "docker:stop": "cd ./docker && docker-compose down", + "server": "node ./server.js" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git" + }, + "keywords": [ + "opentelemetry", + "express", + "tracing" + ], + "engines": { + "node": ">=8" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.map", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js/issues" + }, + "dependencies": { + "@opentelemetry/api": "^1.0.1", + "@opentelemetry/exporter-jaeger": "^0.23.0", + "@opentelemetry/exporter-zipkin": "^0.23.0", + "@opentelemetry/exporter-collector": "^0.23.0", + "@opentelemetry/instrumentation": "^0.23.0", + "@opentelemetry/instrumentation-connect": "^0.23.0", + "@opentelemetry/instrumentation-http": "^0.23.0", + "@opentelemetry/node": "^0.23.0", + "@opentelemetry/resources": "^0.23.0", + "@opentelemetry/semantic-conventions": "^0.23.0", + "@opentelemetry/tracing": "^0.23.0", + "axios": "^0.21.1", + "cross-env": "^7.0.3", + "connect": "^3.7.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme", + "devDependencies": {} +} diff --git a/examples/connect/server.js b/examples/connect/server.js new file mode 100644 index 0000000000..230e5c5646 --- /dev/null +++ b/examples/connect/server.js @@ -0,0 +1,35 @@ +'use strict'; + +// eslint-disable-next-line +const tracing = require('./tracing')('example-connect-server'); + +// Require in rest of modules +const connect = require('connect'); +const axios = require('axios'); + +// Setup express +const app = connect(); +const PORT = 8080; + +app.use(function middleware1(req, res, next) { + next(); +}); + +app.use((req, res, next) => { + next(); +}); + +app.use('/run_test', async (req, res) => { + const result = await axios.get('https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/main/package.json'); + tracing.log('sending response'); + res.end(`OK ${result.data.version}`); + if (tracing.connectInstrumentation.isEnabled()) { + tracing.log('disabling connect'); + tracing.connectInstrumentation.disable(); + } else { + tracing.log('enabling connect'); + tracing.connectInstrumentation.enable(); + } +}); + +app.listen(PORT); diff --git a/examples/connect/tracing.js b/examples/connect/tracing.js new file mode 100644 index 0000000000..85cb967d4e --- /dev/null +++ b/examples/connect/tracing.js @@ -0,0 +1,52 @@ +'use strict'; + +const opentelemetry = require('@opentelemetry/api'); + +const { diag, DiagConsoleLogger, DiagLogLevel } = opentelemetry; +diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); + +const { Resource } = require('@opentelemetry/resources'); +const { ResourceAttributes: SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); +const { NodeTracerProvider } = require('@opentelemetry/node'); +const { SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector'); + +const { ConnectInstrumentation } = require('@opentelemetry/instrumentation-connect'); +const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); + +function log() { + const args = Array.from(arguments) || []; + args.unshift(new Date()); + console.log.apply(this, args); +} + +module.exports = (serviceName) => { + const provider = new NodeTracerProvider({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: serviceName, + }), + }); + const connectInstrumentation = new ConnectInstrumentation(); + registerInstrumentations({ + tracerProvider: provider, + instrumentations: [ + // Connect instrumentation expects HTTP layer to be instrumented + HttpInstrumentation, + connectInstrumentation, + ], + }); + + const exporter = new CollectorTraceExporter(); + + provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + + // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings + provider.register({}); + return { + log, + connectInstrumentation, + provider, + tracer: opentelemetry.trace.getTracer('connect-example'), + } +}; diff --git a/plugins/node/opentelemetry-instrumentation-connect/.eslintignore b/plugins/node/opentelemetry-instrumentation-connect/.eslintignore new file mode 100644 index 0000000000..5498e0f48a --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/.eslintignore @@ -0,0 +1,2 @@ +build +coverage diff --git a/plugins/node/opentelemetry-instrumentation-connect/.eslintrc.js b/plugins/node/opentelemetry-instrumentation-connect/.eslintrc.js new file mode 100644 index 0000000000..f756f4488b --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.config.js') +} diff --git a/plugins/node/opentelemetry-instrumentation-connect/.npmignore b/plugins/node/opentelemetry-instrumentation-connect/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/plugins/node/opentelemetry-instrumentation-connect/LICENSE b/plugins/node/opentelemetry-instrumentation-connect/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/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-connect/README.md b/plugins/node/opentelemetry-instrumentation-connect/README.md new file mode 100644 index 0000000000..3a36b1b5b6 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/README.md @@ -0,0 +1,70 @@ +# OpenTelemetry Connect 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 instrumentation for [`connect`](https://github.com/senchalabs/connect). + +For automatic instrumentation see the +[@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-node) package. + +## Installation + +This instrumentation relies on HTTP calls to also be instrumented. Make sure you install and enable both, otherwise you will not see any spans being exported from the instrumentation. + +```bash +npm install --save @opentelemetry/instrumentation-http @opentelemetry/instrumentation-connect +``` + +### Supported Versions + +- `^3.0.0` + +## Usage + +OpenTelemetry Connect Instrumentation allows the user to automatically collect trace data and export them to their backend of choice, to give observability to distributed systems. + +To load the instrumentation, specify it in the Node Tracer's configuration: + +```js +const { NodeTracerProvider } = require('@opentelemetry/node'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); +const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); +const { ConnectInstrumentation } = require('@opentelemetry/instrumentation-connnect'); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + // Connnect instrumentation expects HTTP layer to be instrumented + new HttpInstrumentation(), + new ConnectInstrumentation(), + ], +}); +``` + +See [examples/connect](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/examples/connect) for a short example. + + +## 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-connect +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=plugins%2Fnode%2Fopentelemetry-instrumentation-connect +[devDependencies-image]: https://status.david-dm.org/gh/open-telemetry/opentelemetry-js-contrib.svg?path=plugins%2Fnode%2Fopentelemetry-instrumentation-connect&type=dev +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=plugins%2Fnode%2Fopentelemetry-instrumentation-connect&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-connect +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-connect.svg diff --git a/plugins/node/opentelemetry-instrumentation-connect/package.json b/plugins/node/opentelemetry-instrumentation-connect/package.json new file mode 100644 index 0000000000..0046249d03 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/package.json @@ -0,0 +1,67 @@ +{ + "name": "@opentelemetry/instrumentation-connect", + "version": "0.23.0", + "description": "OpenTelemetry connect 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 .", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "precompile": "tsc --version", + "prepare": "npm run compile", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "version:update": "node ../../../scripts/version-update.js", + "watch": "tsc -w" + }, + "keywords": [ + "opentelemetry", + "connect", + "nodejs", + "tracing", + "profiling", + "instrumentation" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.5.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.1" + }, + "devDependencies": { + "@opentelemetry/context-async-hooks": "0.23.0", + "@opentelemetry/node": "0.23.0", + "@opentelemetry/tracing": "0.23.0", + "@types/mocha": "7.0.2", + "@types/node": "14.17.4", + "codecov": "3.8.2", + "connect": "^3.7.0", + "gts": "3.1.0", + "mocha": "7.2.0", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "ts-mocha": "8.0.0", + "typescript": "4.3.5" + }, + "dependencies": { + "@opentelemetry/core": "^0.23.0", + "@opentelemetry/instrumentation": "^0.23.0", + "@opentelemetry/semantic-conventions": "^0.23.0", + "@types/connect": "3.4.35" + } +} diff --git a/plugins/node/opentelemetry-instrumentation-connect/src/enums/AttributeNames.ts b/plugins/node/opentelemetry-instrumentation-connect/src/enums/AttributeNames.ts new file mode 100644 index 0000000000..087717b854 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/src/enums/AttributeNames.ts @@ -0,0 +1,30 @@ +/* + * 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 { + CONNECT_TYPE = 'connect.type', + CONNECT_NAME = 'connect.name', +} + +export enum ConnectTypes { + MIDDLEWARE = 'middleware', + REQUEST_HANDLER = 'request_handler', +} + +export enum ConnectNames { + MIDDLEWARE = 'middleware', + REQUEST_HANDLER = 'request handler', +} diff --git a/plugins/node/opentelemetry-instrumentation-connect/src/index.ts b/plugins/node/opentelemetry-instrumentation-connect/src/index.ts new file mode 100644 index 0000000000..16b659dde3 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/src/index.ts @@ -0,0 +1,19 @@ +/* + * 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 './enums/AttributeNames'; +export * from './instrumentation'; +export * from './types'; diff --git a/plugins/node/opentelemetry-instrumentation-connect/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-connect/src/instrumentation.ts new file mode 100644 index 0000000000..6d48b3cfd0 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/src/instrumentation.ts @@ -0,0 +1,174 @@ +/* + * 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, diag, Span, SpanOptions } from '@opentelemetry/api'; +import { getRPCMetadata, RPCType } from '@opentelemetry/core'; +import type { HandleFunction, NextFunction, Server } from 'connect'; +import type { IncomingMessage, ServerResponse } from 'http'; +import { + AttributeNames, + ConnectNames, + ConnectTypes, +} from './enums/AttributeNames'; +import { Use, UseArgs, UseArgs2 } from './types'; +import { VERSION } from './version'; +import { + InstrumentationBase, + InstrumentationConfig, + InstrumentationNodeModuleDefinition, + isWrapped, +} from '@opentelemetry/instrumentation'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +export const ANONYMOUS_NAME = 'anonymous'; + +/** Connect instrumentation for OpenTelemetry */ +export class ConnectInstrumentation extends InstrumentationBase { + ENABLED: boolean = false; + constructor(config: InstrumentationConfig = {}) { + super( + '@opentelemetry/instrumentation-connect', + VERSION, + Object.assign({}, config) + ); + } + + init() { + return [ + new InstrumentationNodeModuleDefinition( + 'connect', + ['^3.0.0'], + (moduleExports, moduleVersion) => { + diag.debug(`Applying patch for connect@${moduleVersion}`); + return this._patchConstructor(moduleExports); + }, + (moduleExports, moduleVersion) => { + diag.debug(`Removing patch for connect@${moduleVersion}`); + } + ), + ]; + } + + private _patchApp(patchedApp: Server) { + if (!isWrapped(patchedApp.use)) { + this._wrap(patchedApp, 'use', this._patchUse.bind(this)); + } + } + + private _patchConstructor(original: () => Server): () => Server { + const instrumentation = this; + return function (this: Server, ...args) { + const app = original.apply(this, args) as Server; + instrumentation._patchApp(app); + return app; + }; + } + + public _patchNext(next: NextFunction, finishSpan: () => void): NextFunction { + return function nextFunction(this: NextFunction, err?: any): void { + const result = next.apply(this, err); + finishSpan(); + return result; + }; + } + + public _startSpan(routeName: string, middleWare: HandleFunction): Span { + let connectType: ConnectTypes; + let connectName: string; + let connectTypeName: string; + if (routeName) { + connectType = ConnectTypes.REQUEST_HANDLER; + connectTypeName = ConnectNames.REQUEST_HANDLER; + connectName = routeName; + } else { + connectType = ConnectTypes.MIDDLEWARE; + connectTypeName = ConnectNames.MIDDLEWARE; + connectName = middleWare.name || ANONYMOUS_NAME; + } + const spanName = `${connectTypeName} - ${connectName}`; + const options: SpanOptions = { + attributes: { + [SemanticAttributes.HTTP_ROUTE]: routeName.length > 0 ? routeName : '/', + [AttributeNames.CONNECT_TYPE]: connectType, + [AttributeNames.CONNECT_NAME]: connectName, + }, + }; + + return this.tracer.startSpan(spanName, options); + } + + public _patchMiddleware( + routeName: string, + middleWare: HandleFunction + ): HandleFunction { + const instrumentation = this; + return function (this: Use): void { + if (!instrumentation.isEnabled()) { + return (middleWare as any).apply(this, arguments); + } + const req = arguments[0] as IncomingMessage; + const res = arguments[1] as ServerResponse; + const next = arguments[2] as NextFunction; + + const rpcMetadata = getRPCMetadata(context.active()); + if (routeName && rpcMetadata?.type === RPCType.HTTP) { + rpcMetadata.span.updateName(`${req.method} ${routeName || '/'}`); + } + let spanName = ''; + if (routeName) { + spanName = `request handler - ${routeName}`; + } else { + spanName = `middleware - ${middleWare.name || ANONYMOUS_NAME}`; + } + const span = instrumentation._startSpan(routeName, middleWare); + instrumentation._diag.debug('start span', spanName); + let spanFinished = false; + + function finishSpan() { + if (!spanFinished) { + spanFinished = true; + instrumentation._diag.debug(`finishing span ${(span as any).name}`); + span.end(); + } else { + instrumentation._diag.debug( + `span ${(span as any).name} - already finished` + ); + } + res.off('close', finishSpan); + } + + res.once('close', finishSpan); + arguments[2] = instrumentation._patchNext(next, finishSpan); + + return (middleWare as any).apply(this, arguments); + }; + } + + public _patchUse(original: Server['use']): Use { + const instrumentation = this; + return function (this: Server, ...args: UseArgs): Server { + const middleWare = args[args.length - 1] as HandleFunction; + const routeName = (args[args.length - 2] || '') as string; + + args[args.length - 1] = instrumentation._patchMiddleware( + routeName, + middleWare + ); + + return original.apply(this, args as UseArgs2); + }; + } +} diff --git a/plugins/node/opentelemetry-instrumentation-connect/src/types.ts b/plugins/node/opentelemetry-instrumentation-connect/src/types.ts new file mode 100644 index 0000000000..15947a401f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/src/types.ts @@ -0,0 +1,22 @@ +/* + * 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 type { HandleFunction, Server } from 'connect'; + +export type UseArgs1 = [HandleFunction]; +export type UseArgs2 = [string, HandleFunction]; +export type UseArgs = UseArgs1 | UseArgs2; +export type Use = (...args: UseArgs) => Server; diff --git a/plugins/node/opentelemetry-instrumentation-connect/src/version.ts b/plugins/node/opentelemetry-instrumentation-connect/src/version.ts new file mode 100644 index 0000000000..113a67d8b1 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/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.23.0'; diff --git a/plugins/node/opentelemetry-instrumentation-connect/test/instrumentation.test.ts b/plugins/node/opentelemetry-instrumentation-connect/test/instrumentation.test.ts new file mode 100644 index 0000000000..6cd177504c --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/test/instrumentation.test.ts @@ -0,0 +1,218 @@ +/* + * 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 assert from 'assert'; + +import { context, trace } from '@opentelemetry/api'; +import { RPCType, setRPCMetadata } from '@opentelemetry/core'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import * as http from 'http'; +import { Socket } from 'net'; +import { ANONYMOUS_NAME, ConnectInstrumentation } from '../src'; + +const httpRequest = { + get: (options: http.ClientRequestArgs | string) => { + return new Promise((resolve, reject) => { + return http.get(options, resp => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + resolve(data); + }); + resp.on('error', err => { + reject(err); + }); + }); + }); + }, +}; + +const getNextPort = async (port: number) => { + return new Promise((resolve, reject) => { + const socket = new Socket(); + const timeout = () => { + resolve(port); + socket.destroy(); + }; + const next = () => { + socket.destroy(); + resolve(getNextPort(++port)); + }; + + setTimeout(timeout, 20); + socket.on('timeout', timeout); + socket.on('connect', () => { + next(); + }); + socket.on('error', (exception: any) => { + if (exception.code !== 'ECONNREFUSED') { + reject(exception); + } else { + next(); + } + }); + socket.connect(port, '0.0.0.0'); + }); +}; + +const instrumentation = new ConnectInstrumentation(); +const contextManager = new AsyncHooksContextManager().enable(); +const memoryExporter = new InMemorySpanExporter(); +const provider = new NodeTracerProvider(); +const spanProcessor = new SimpleSpanProcessor(memoryExporter); +instrumentation.setTracerProvider(provider); +context.setGlobalContextManager(contextManager); + +const tracer = provider.getTracer('default'); + +provider.addSpanProcessor(spanProcessor); +instrumentation.enable(); + +import * as connect from 'connect'; + +describe('connect', () => { + let PORT: number; + let app: connect.Server; + let server: http.Server; + + before(async () => { + PORT = (await getNextPort(8081)) as number; + }); + + beforeEach(() => { + instrumentation.enable(); + app = connect(); + server = app.listen(PORT); + }); + + afterEach(() => { + app.removeAllListeners(); + server.close(); + contextManager.disable(); + contextManager.enable(); + memoryExporter.reset(); + instrumentation.disable(); + }); + + describe('when connect is disabled', () => { + it('should not generate any spans', async () => { + instrumentation.disable(); + app.use((req, res, next) => { + next(); + }); + + await httpRequest.get(`http://localhost:${PORT}/`); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + }); + describe('when connect is enabled', () => { + it('should generate span for anonymous middleware', async () => { + app.use((req, res, next) => { + next(); + }); + + await httpRequest.get(`http://localhost:${PORT}/`); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.deepStrictEqual(span.attributes, { + 'connect.type': 'middleware', + 'connect.name': ANONYMOUS_NAME, + [SemanticAttributes.HTTP_ROUTE]: '/', + }); + assert.strictEqual(span.name, 'middleware - anonymous'); + }); + + it('should generate span for named middleware', async () => { + // eslint-disable-next-line prefer-arrow-callback + app.use(function middleware1(req, res, next) { + next(); + }); + + await httpRequest.get(`http://localhost:${PORT}/`); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.deepStrictEqual(span.attributes, { + 'connect.type': 'middleware', + 'connect.name': 'middleware1', + [SemanticAttributes.HTTP_ROUTE]: '/', + }); + assert.strictEqual(span.name, 'middleware - middleware1'); + }); + + it('should generate span for route', async () => { + app.use('/foo', (req, res, next) => { + next(); + }); + + await httpRequest.get(`http://localhost:${PORT}/foo`); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.deepStrictEqual(span.attributes, { + 'connect.type': 'request_handler', + 'connect.name': '/foo', + [SemanticAttributes.HTTP_ROUTE]: '/foo', + }); + assert.strictEqual(span.name, 'request handler - /foo'); + }); + + it('should change name for parent http route', async () => { + const rootSpan = tracer.startSpan('root span'); + app.use((req, res, next) => { + const rpcMetadata = { type: RPCType.HTTP, span: rootSpan }; + return context.with( + setRPCMetadata( + trace.setSpan(context.active(), rootSpan), + rpcMetadata + ), + next + ); + }); + + app.use('/foo', (req, res, next) => { + next(); + }); + + await httpRequest.get(`http://localhost:${PORT}/foo`); + rootSpan.end(); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 3); + const changedRootSpan = spans[2]; + const span = spans[0]; + assert.strictEqual(changedRootSpan.name, 'GET /foo'); + assert.strictEqual(span.name, 'request handler - /foo'); + assert.strictEqual( + span.parentSpanId, + changedRootSpan.spanContext().spanId + ); + }); + }); +}); diff --git a/plugins/node/opentelemetry-instrumentation-connect/tsconfig.json b/plugins/node/opentelemetry-instrumentation-connect/tsconfig.json new file mode 100644 index 0000000000..28be80d266 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-connect/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +}