diff --git a/examples/connect/client.js b/examples/connect/client.js index dc34f77e6c..265710d3b1 100644 --- a/examples/connect/client.js +++ b/examples/connect/client.js @@ -2,7 +2,8 @@ // eslint-disable-next-line import/order const tracing = require('./tracing')('example-connect-client'); -const tracer = tracing.tracer; + +const { tracer } = tracing; const api = require('@opentelemetry/api'); const axios = require('axios').default; diff --git a/examples/connect/server.js b/examples/connect/server.js index a92bb092f6..0c70584891 100644 --- a/examples/connect/server.js +++ b/examples/connect/server.js @@ -11,6 +11,7 @@ const axios = require('axios'); const app = connect(); const PORT = 8080; +// eslint-disable-next-line prefer-arrow-callback app.use(function middleware1(req, res, next) { next(); }); @@ -32,7 +33,6 @@ app.use('/run_test', async (req, res) => { tracing.log('enabling connect'); tracing.connectInstrumentation.enable(); } - }); app.listen(PORT); diff --git a/examples/connect/tracing.js b/examples/connect/tracing.js index 9564e85a16..d711b935d1 100644 --- a/examples/connect/tracing.js +++ b/examples/connect/tracing.js @@ -48,5 +48,5 @@ module.exports = (serviceName) => { connectInstrumentation, provider, tracer: opentelemetry.trace.getTracer('connect-example'), - } + }; }; diff --git a/examples/fastify/README.md b/examples/fastify/README.md new file mode 100644 index 0000000000..bde7fff0bc --- /dev/null +++ b/examples/fastify/README.md @@ -0,0 +1,56 @@ +# Overview + +OpenTelemetry Fastify 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 Fastify 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/fastify/client.js b/examples/fastify/client.js new file mode 100644 index 0000000000..32f460ab4f --- /dev/null +++ b/examples/fastify/client.js @@ -0,0 +1,39 @@ +'use strict'; + +// eslint-disable-next-line import/order +const tracing = require('./tracing')('example-fastify-client'); + +const { tracer } = tracing; +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/1', { + // testing + // const res = await axios.post('http://localhost:8080/run_test2/1', { + headers: { + 'Content-Type': 'application/json', + }, + timeout: 3000, + }); + 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/fastify/docker/collector-config.yaml b/examples/fastify/docker/collector-config.yaml new file mode 100644 index 0000000000..04d65a6ba2 --- /dev/null +++ b/examples/fastify/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/fastify/docker/docker-compose.yaml b/examples/fastify/docker/docker-compose.yaml new file mode 100644 index 0000000000..396ca636ec --- /dev/null +++ b/examples/fastify/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/fastify/images/trace1.png b/examples/fastify/images/trace1.png new file mode 100644 index 0000000000..c850b1f72a Binary files /dev/null and b/examples/fastify/images/trace1.png differ diff --git a/examples/fastify/package.json b/examples/fastify/package.json new file mode 100644 index 0000000000..822fbe4eb8 --- /dev/null +++ b/examples/fastify/package.json @@ -0,0 +1,59 @@ +{ + "name": "example-fastify", + "private": true, + "version": "0.25.0", + "description": "Example of Fastify 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.2", + "@opentelemetry/exporter-jaeger": "^1.0.0", + "@opentelemetry/exporter-zipkin": "^1.0.0", + "@opentelemetry/exporter-collector": "^0.25.0", + "@opentelemetry/instrumentation": "^0.26.0", + "@opentelemetry/instrumentation-fastify": "^0.25.0", + "@opentelemetry/instrumentation-http": "^0.26.0", + "@opentelemetry/sdk-trace-node": "^1.0.0", + "@opentelemetry/resources": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@opentelemetry/sdk-trace-base": "^1.0.0", + "axios": "^0.21.1", + "cross-env": "^7.0.3", + "fastify": "^3.19.2", + "fastify-cors": "^6.0.2", + "fastify-express": "^0.3.3", + "middie": "^5.3.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme", + "devDependencies": {} +} diff --git a/examples/fastify/server.js b/examples/fastify/server.js new file mode 100644 index 0000000000..deb99a4cd7 --- /dev/null +++ b/examples/fastify/server.js @@ -0,0 +1,76 @@ +'use strict'; + +// eslint-disable-next-line +const tracing = require('./tracing')('example-fastify-server'); +const opentelemetry = require('@opentelemetry/api'); + +const { context, trace } = opentelemetry; +const Fastify = require('fastify'); +const axios = require('axios'); + +const PORT = 8080; +const app = Fastify({ logger: true }); +app + .register(require('fastify-express')) + .register(subsystem); + +async function subsystem(fastify) { + fastify.addHook('onRequest', async () => { + const span = trace.getSpan(context.active()); + span.setAttribute('order', 2); + }); + + // eslint-disable-next-line prefer-arrow-callback + fastify.addHook('onRequest', async function onRequestHook() { + const span = trace.getSpan(context.active()); + span.setAttribute('order', 3); + + const newSpan = tracing.tracer.startSpan('foo'); + newSpan.setAttribute('foo', 'bar'); + newSpan.end(); + }); + + fastify.use((req, res, next) => { + const span = trace.getSpan(context.active()); + span.setAttribute('order', 1); + next(); + }); + + fastify.post('/run_test2/:id', async (req, res) => { + const span = trace.getSpan(context.active()); + span.setAttribute('order', 4); + + const result = await axios.get('https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/main/package.json'); + const result2 = await axios.get('https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/main/package.json'); + + tracing.log('sending response'); + // throw Error('boom lala'); + res.send(`OK ${result.data.version} ${result2.data.version}`); + }); + + fastify.addHook('onRequest', (req, reply, done) => { + const span = trace.getSpan(context.active()); + console.log('first', span); + console.log('kuku1'); + span.setAttribute('kuku1', 'lala'); + + setTimeout(() => { + console.log('kuku2'); + span.setAttribute('kuku2', 'lala'); + const newSpan = tracing.tracer.startSpan('tada'); + newSpan.end(); + + reply.send('foo'); + done(); + }, 2000); + }); + +} + +app.post('/run_test/:id', async (req, res) => { + const result = await axios.get('https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/main/package.json'); + tracing.log('sending response'); + res.send(`OK ${result.data.version}`); +}); + +app.listen(PORT); diff --git a/examples/fastify/tracing.js b/examples/fastify/tracing.js new file mode 100644 index 0000000000..fdc4337f24 --- /dev/null +++ b/examples/fastify/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 { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base'); +const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector'); + +const { FastifyInstrumentation } = require('@opentelemetry/instrumentation-fastify'); +const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); + +function log() { + // eslint-disable-next-line prefer-rest-params + 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 fastifyInstrumentation = new FastifyInstrumentation(); + registerInstrumentations({ + tracerProvider: provider, + instrumentations: [ + // Fastify instrumentation expects HTTP layer to be instrumented + HttpInstrumentation, + fastifyInstrumentation, + ], + }); + + const exporter = new CollectorTraceExporter(); + provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + + // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings + provider.register({}); + return { + log, + fastifyInstrumentation, + provider, + tracer: opentelemetry.trace.getTracer('fastify-example'), + }; +}; diff --git a/examples/grpc_dynamic_codegen/tracer.js b/examples/grpc_dynamic_codegen/tracer.js index 99931ef264..768edfab1e 100644 --- a/examples/grpc_dynamic_codegen/tracer.js +++ b/examples/grpc_dynamic_codegen/tracer.js @@ -8,7 +8,6 @@ const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const { GrpcInstrumentation } = require('@opentelemetry/instrumentation-grpc'); - const EXPORTER = process.env.EXPORTER || ''; module.exports = (serviceName) => { diff --git a/examples/mongodb/server.js b/examples/mongodb/server.js index c97b354178..082f599976 100644 --- a/examples/mongodb/server.js +++ b/examples/mongodb/server.js @@ -68,6 +68,7 @@ function handleInsertQuery(response) { } else { console.log('1 document inserted'); // find document to test context propagation using callback + // eslint-disable-next-line prefer-arrow-callback collection.findOne({}, function () { response.end(); }); diff --git a/examples/web/webpack.config.js b/examples/web/webpack.config.js index a431e9e2b6..fd3de03bb4 100644 --- a/examples/web/webpack.config.js +++ b/examples/web/webpack.config.js @@ -10,7 +10,7 @@ const common = { mode: 'development', entry: { 'document-load': 'examples/document-load/index.js', - 'meta': 'examples/meta/index.js', + meta: 'examples/meta/index.js', 'user-interaction': 'examples/user-interaction/index.js', }, output: { diff --git a/plugins/node/opentelemetry-instrumentation-fastify/.eslintignore b/plugins/node/opentelemetry-instrumentation-fastify/.eslintignore new file mode 100644 index 0000000000..5498e0f48a --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/.eslintignore @@ -0,0 +1,2 @@ +build +coverage diff --git a/plugins/node/opentelemetry-instrumentation-fastify/.eslintrc.js b/plugins/node/opentelemetry-instrumentation-fastify/.eslintrc.js new file mode 100644 index 0000000000..f756f4488b --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.config.js') +} diff --git a/plugins/node/opentelemetry-instrumentation-fastify/.npmignore b/plugins/node/opentelemetry-instrumentation-fastify/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/plugins/node/opentelemetry-instrumentation-fastify/LICENSE b/plugins/node/opentelemetry-instrumentation-fastify/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/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-fastify/README.md b/plugins/node/opentelemetry-instrumentation-fastify/README.md new file mode 100644 index 0000000000..fd9c9a96a1 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/README.md @@ -0,0 +1,70 @@ +# OpenTelemetry Fastify 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 [`fastify`](https://www.fastify.io/). + +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 have spans that are not connected with each other. + +```bash +npm install --save @opentelemetry/instrumentation-http @opentelemetry/instrumentation-fastify +``` + +### Supported Versions + +- `^3.0.0` + +## Usage + +OpenTelemetry fastify 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 { FastifyInstrumentation } = require('@opentelemetry/instrumentation-fastify'); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + // Fastify instrumentation expects HTTP layer to be instrumented + new HttpInstrumentation(), + new FastifyInstrumentation(), + ], +}); +``` + +See [examples/fastify](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/examples/fastify) 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-fastify +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=plugins%2Fnode%2Fopentelemetry-instrumentation-fastify +[devDependencies-image]: https://status.david-dm.org/gh/open-telemetry/opentelemetry-js-contrib.svg?path=plugins%2Fnode%2Fopentelemetry-instrumentation-fastify&type=dev +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=plugins%2Fnode%2Fopentelemetry-instrumentation-fastify&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-fastify +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-fastify.svg diff --git a/plugins/node/opentelemetry-instrumentation-fastify/package.json b/plugins/node/opentelemetry-instrumentation-fastify/package.json new file mode 100644 index 0000000000..a86755e15f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/package.json @@ -0,0 +1,71 @@ +{ + "name": "@opentelemetry/instrumentation-fastify", + "version": "0.25.0", + "description": "OpenTelemetry fastly 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", + "fastify", + "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.2" + }, + "devDependencies": { + "@opentelemetry/api": "1.0.2", + "@opentelemetry/context-async-hooks": "1.0.0", + "@opentelemetry/instrumentation-http": "0.26.0", + "@opentelemetry/sdk-trace-node": "1.0.0", + "@opentelemetry/sdk-trace-base": "1.0.0", + "@types/express": "4.17.13", + "@types/mocha": "7.0.2", + "@types/node": "14.17.4", + "codecov": "3.8.2", + "fastify-express": "0.3.3", + "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": "^1.0.0", + "@opentelemetry/instrumentation": "^0.26.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "fastify": "^3.19.2" + } +} diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/constants.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/constants.ts new file mode 100644 index 0000000000..e6e0ebedae --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/constants.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 const spanRequestSymbol = Symbol( + 'opentelemetry.instrumentation.fastify.request_active_span' +); + +export const applicationHookNames = [ + 'onRegister', + 'onRoute', + 'onReady', + 'onClose', +]; diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/enums/AttributeNames.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/enums/AttributeNames.ts new file mode 100644 index 0000000000..3626fb8fab --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/enums/AttributeNames.ts @@ -0,0 +1,32 @@ +/* + * 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 { + FASTIFY_NAME = 'fastify.name', + FASTIFY_TYPE = 'fastify.type', + HOOK_NAME = 'hook.name', + PLUGIN_NAME = 'plugin.name', +} + +export enum FastifyTypes { + MIDDLEWARE = 'middleware', + REQUEST_HANDLER = 'request_handler', +} + +export enum FastifyNames { + MIDDLEWARE = 'middleware', + REQUEST_HANDLER = 'request handler', +} diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/index.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/index.ts new file mode 100644 index 0000000000..34b600dd0c --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/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 * from './enums/AttributeNames'; +export * from './instrumentation'; diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts new file mode 100644 index 0000000000..985238a0b4 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts @@ -0,0 +1,270 @@ +/* + * 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, + SpanAttributes, + SpanStatusCode, + trace, +} from '@opentelemetry/api'; +import { getRPCMetadata, RPCType } from '@opentelemetry/core'; +import { + InstrumentationBase, + InstrumentationConfig, + InstrumentationNodeModuleDefinition, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { HookHandlerDoneFunction } from 'fastify/types/hooks'; +import type { FastifyInstance } from 'fastify/types/instance'; +import type { FastifyReply } from 'fastify/types/reply'; +import type { FastifyRequest } from 'fastify/types/request'; +import { applicationHookNames } from './constants'; +import { + AttributeNames, + FastifyNames, + FastifyTypes, +} from './enums/AttributeNames'; +import type { HandlerOriginal, PluginFastifyReply } from './types'; +import { + endSpan, + safeExecuteInTheMiddleMaybePromise, + startSpan, +} from './utils'; +import { VERSION } from './version'; + +export const ANONYMOUS_NAME = 'anonymous'; + +/** Fastify instrumentation for OpenTelemetry */ +export class FastifyInstrumentation extends InstrumentationBase { + constructor(config: InstrumentationConfig = {}) { + super( + '@opentelemetry/instrumentation-fastify', + VERSION, + Object.assign({}, config) + ); + } + + init() { + return [ + new InstrumentationNodeModuleDefinition( + 'fastify', + ['^3.0.0'], + (moduleExports, moduleVersion) => { + this._diag.debug(`Applying patch for fastify@${moduleVersion}`); + return this._patchConstructor(moduleExports); + } + ), + ]; + } + + private _hookOnRequest() { + const instrumentation = this; + return function onRequest( + request: FastifyRequest, + reply: FastifyReply, + done: HookHandlerDoneFunction + ) { + if (!instrumentation.isEnabled()) { + return done(); + } + instrumentation._wrap(reply, 'send', instrumentation._patchSend()); + + const rpcMetadata = getRPCMetadata(context.active()); + const routeName = request.routerPath; + if (routeName && rpcMetadata?.type === RPCType.HTTP) { + rpcMetadata.span.setAttribute(SemanticAttributes.HTTP_ROUTE, routeName); + rpcMetadata.span.updateName(`${request.method} ${routeName || '/'}`); + } + done(); + }; + } + + private _wrapHandler( + pluginName: string, + hookName: string, + original: (...args: unknown[]) => Promise | void, + syncFunctionWithDone: boolean + ): () => Promise | void { + const instrumentation = this; + return function (this: any, ...args: unknown[]): Promise | void { + if (!instrumentation.isEnabled()) { + return original.apply(this, args); + } + + const spanName = `${FastifyNames.MIDDLEWARE} - ${ + original.name || ANONYMOUS_NAME + }`; + + const reply = args[1] as PluginFastifyReply; + + const span = startSpan(reply, instrumentation.tracer, spanName, { + [AttributeNames.FASTIFY_TYPE]: FastifyTypes.MIDDLEWARE, + [AttributeNames.PLUGIN_NAME]: pluginName, + [AttributeNames.HOOK_NAME]: hookName, + }); + + const origDone = + syncFunctionWithDone && + (args[args.length - 1] as HookHandlerDoneFunction); + if (origDone) { + args[args.length - 1] = function ( + ...doneArgs: Parameters + ) { + endSpan(reply); + origDone.apply(this, doneArgs); + }; + } + + return context.with(trace.setSpan(context.active(), span), () => { + return safeExecuteInTheMiddleMaybePromise( + () => { + return original.apply(this, args); + }, + err => { + if (err) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + span.recordException(err); + } + // async hooks should end the span as soon as the promise is resolved + if (!syncFunctionWithDone) { + endSpan(reply); + } + } + ); + }); + }; + } + + private _wrapAddHook(): ( + original: FastifyInstance['addHook'] + ) => () => FastifyInstance { + const instrumentation = this; + return function ( + original: FastifyInstance['addHook'] + ): () => FastifyInstance { + return function wrappedAddHook(this: any, ...args: any) { + const name = args[0] as string; + const handler = args[1] as HandlerOriginal; + const pluginName = this.pluginName; + if (applicationHookNames.includes(name)) { + return original.apply(this, [name as any, handler]); + } + + const syncFunctionWithDone = + typeof args[args.length - 1] === 'function' && + handler.constructor.name !== 'AsyncFunction'; + + return original.apply(this, [ + name as any, + instrumentation._wrapHandler( + pluginName, + name, + handler, + syncFunctionWithDone + ), + ]); + }; + }; + } + + private _patchConstructor( + original: () => FastifyInstance + ): () => FastifyInstance { + const instrumentation = this; + + function fastify(this: FastifyInstance, ...args: any) { + const app: FastifyInstance = original.apply(this, args); + app.addHook('onRequest', instrumentation._hookOnRequest()); + app.addHook('preHandler', instrumentation._hookPreHandler()); + + instrumentation._wrap(app, 'addHook', instrumentation._wrapAddHook()); + + return app; + } + + fastify.fastify = fastify; + fastify.default = fastify; + return fastify; + } + + public _patchSend() { + const instrumentation = this; + return function patchSend( + original: () => FastifyReply + ): () => FastifyReply { + return function send(this: FastifyReply, ...args: any) { + const maybeError: any = args[0]; + + if (!instrumentation.isEnabled()) { + return original.apply(this, args); + } + + return safeExecuteInTheMiddle( + () => { + return original.apply(this, args); + }, + err => { + if (!err && maybeError instanceof Error) { + err = maybeError; + } + endSpan(this, err); + } + ); + }; + }; + } + + public _hookPreHandler() { + const instrumentation = this; + return function preHandler( + this: any, + request: FastifyRequest, + reply: FastifyReply, + done: HookHandlerDoneFunction + ) { + if (!instrumentation.isEnabled()) { + return done(); + } + const requestContext = (request as any).context || {}; + const handlerName = (requestContext.handler?.name || '').substr(6); + const spanName = `${FastifyNames.REQUEST_HANDLER} - ${ + handlerName || ANONYMOUS_NAME + }`; + + const spanAttributes: SpanAttributes = { + [AttributeNames.PLUGIN_NAME]: this.pluginName, + [AttributeNames.FASTIFY_TYPE]: FastifyTypes.REQUEST_HANDLER, + [SemanticAttributes.HTTP_ROUTE]: request.routerPath, + }; + if (handlerName) { + spanAttributes[AttributeNames.FASTIFY_NAME] = handlerName; + } + const span = startSpan( + reply, + instrumentation.tracer, + spanName, + spanAttributes + ); + return context.with(trace.setSpan(context.active(), span), () => { + done(); + }); + }; + } +} diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/types.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/types.ts new file mode 100644 index 0000000000..ee2e325494 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/types.ts @@ -0,0 +1,25 @@ +/* + * 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 { FastifyReply } from 'fastify'; +import { spanRequestSymbol } from './constants'; + +export type HandlerOriginal = (() => Promise) & (() => void); + +export type PluginFastifyReply = FastifyReply & { + [spanRequestSymbol]?: Span[]; +}; diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/utils.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/utils.ts new file mode 100644 index 0000000000..7b328fea3c --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/utils.ts @@ -0,0 +1,130 @@ +/* + * 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, + SpanAttributes, + SpanStatusCode, + Tracer, +} from '@opentelemetry/api'; +import { spanRequestSymbol } from './constants'; + +import type { PluginFastifyReply } from './types'; + +/** + * Starts Span + * @param reply - reply function + * @param tracer - tracer + * @param spanName - span name + * @param spanAttributes - span attributes + */ +export function startSpan( + reply: PluginFastifyReply, + tracer: Tracer, + spanName: string, + spanAttributes: SpanAttributes = {} +) { + const span = tracer.startSpan(spanName, { attributes: spanAttributes }); + + const spans: Span[] = reply[spanRequestSymbol] || []; + spans.push(span); + + Object.defineProperty(reply, spanRequestSymbol, { + enumerable: false, + configurable: true, + value: spans, + }); + + return span; +} + +/** + * Ends span + * @param reply - reply function + * @param err - error + */ +export function endSpan(reply: PluginFastifyReply, err?: any) { + const spans = reply[spanRequestSymbol] || []; + // there is no active span, or it has already ended + if (!spans.length) { + return; + } + spans.forEach(span => { + if (err) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + span.recordException(err); + } + span.end(); + }); + delete reply[spanRequestSymbol]; +} + +// @TODO after approve add this to instrumentation package and replace usage +// when it will be released + +/** + * This function handles the missing case from instrumentation package when + * execute can either return a promise or void. And using async is not an + * option as it is producing unwanted side effects. + * @param execute - function to be executed + * @param onFinish - function called when function executed + * @param preventThrowingError - prevent to throw error when execute + * function fails + */ +export function safeExecuteInTheMiddleMaybePromise( + execute: () => Promise | T, + onFinish: (e: Error | undefined, result?: T) => void, + preventThrowingError?: boolean +): Promise | T | void { + let error: Error | undefined; + let executeResult: Promise | T | void; + let isPromise = false; + let result: T | undefined = undefined; + try { + executeResult = execute(); + const promiseResult = executeResult as Promise; + + isPromise = promiseResult && typeof promiseResult.then === 'function'; + + if (isPromise) { + promiseResult.then( + res => { + onFinish(undefined, res); + }, + (err: Error) => { + onFinish(err); + } + ); + } else { + result = executeResult as T | undefined; + } + } catch (e) { + error = e; + } finally { + if (!isPromise) { + onFinish(error, result); + if (error && !preventThrowingError) { + // eslint-disable-next-line no-unsafe-finally + throw error; + } + } + // eslint-disable-next-line no-unsafe-finally + return executeResult; + } +} diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/version.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/version.ts new file mode 100644 index 0000000000..2c1c3f80c6 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/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.25.0'; diff --git a/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts b/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts new file mode 100644 index 0000000000..503606d296 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts @@ -0,0 +1,426 @@ +/* + * 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, ROOT_CONTEXT, SpanStatusCode } from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { + InMemorySpanExporter, + ReadableSpan, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import { HookHandlerDoneFunction } from 'fastify/types/hooks'; +import { FastifyReply } from 'fastify/types/reply'; +import { FastifyRequest } from 'fastify/types/request'; +import * as http from 'http'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { ANONYMOUS_NAME } from '../src/instrumentation'; +import { AttributeNames, FastifyInstrumentation } from '../src'; + +const URL = require('url').URL; + +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 httpInstrumentation = new HttpInstrumentation(); +const instrumentation = new FastifyInstrumentation(); +const contextManager = new AsyncHooksContextManager().enable(); +const memoryExporter = new InMemorySpanExporter(); +const provider = new NodeTracerProvider(); +const spanProcessor = new SimpleSpanProcessor(memoryExporter); +instrumentation.setTracerProvider(provider); +httpInstrumentation.setTracerProvider(provider); +context.setGlobalContextManager(contextManager); + +provider.addSpanProcessor(spanProcessor); +instrumentation.enable(); +httpInstrumentation.enable(); + +import 'fastify-express'; +import { FastifyInstance } from 'fastify/types/instance'; + +const Fastify = require('fastify'); + +function getSpans(): ReadableSpan[] { + const spans = memoryExporter.getFinishedSpans().filter(s => { + return ( + s.instrumentationLibrary.name === '@opentelemetry/instrumentation-fastify' + ); + }); + return spans; +} + +describe('fastify', () => { + let PORT: number; + let app: FastifyInstance; + + function startServer(): Promise { + return new Promise(resolve => + app.listen(0, (err, address) => { + const url = new URL(address); + PORT = parseInt(url.port, 10); + resolve(); + }) + ); + } + + beforeEach(async () => { + instrumentation.enable(); + app = Fastify(); + app.register(require('fastify-express')); + }); + + afterEach(async () => { + await new Promise(resolve => + app.close(() => { + resolve(); + }) + ); + + contextManager.disable(); + contextManager.enable(); + memoryExporter.reset(); + instrumentation.disable(); + }); + + describe('when fastify is disabled', () => { + it('should not generate any spans', async () => { + instrumentation.disable(); + app.get('/test', (req, res) => { + res.send('OK'); + }); + await startServer(); + + await httpRequest.get(`http://localhost:${PORT}/test`); + + const spans = getSpans(); + assert.strictEqual(spans.length, 0); // http instrumentation only + }); + }); + + describe('when fastify is enabled', () => { + it('should generate span for anonymous middleware', async () => { + app.get('/test', (req, res) => { + res.send('OK'); + }); + + await startServer(); + + await httpRequest.get(`http://localhost:${PORT}/test`); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 5); + const span = spans[3]; + assert.deepStrictEqual(span.attributes, { + 'fastify.type': 'request_handler', + 'plugin.name': 'fastify-express', + [SemanticAttributes.HTTP_ROUTE]: '/test', + }); + assert.strictEqual(span.name, `request handler - ${ANONYMOUS_NAME}`); + const baseSpan = spans[1]; + assert.strictEqual(span.parentSpanId, baseSpan.spanContext().spanId); + }); + + it('should generate span for named handler', async () => { + // eslint-disable-next-line prefer-arrow-callback + app.get('/test', function namedHandler(req, res) { + res.send('OK'); + }); + + await startServer(); + + await httpRequest.get(`http://localhost:${PORT}/test`); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 5); + const span = spans[3]; + assert.deepStrictEqual(span.attributes, { + 'fastify.type': 'request_handler', + 'fastify.name': 'namedHandler', + 'plugin.name': 'fastify-express', + [SemanticAttributes.HTTP_ROUTE]: '/test', + }); + assert.strictEqual(span.name, 'request handler - namedHandler'); + + const baseSpan = spans[1]; + assert.strictEqual(span.parentSpanId, baseSpan.spanContext().spanId); + }); + + describe('when subsystem is registered', () => { + beforeEach(async () => { + httpInstrumentation.enable(); + + async function subsystem(fastify: FastifyInstance) { + fastify.addHook( + 'onRequest', + async ( + req: FastifyRequest, + res: FastifyReply, + next: HookHandlerDoneFunction + ) => { + next(); + } + ); + fastify.use((req, res, next) => { + next(); + }); + // eslint-disable-next-line prefer-arrow-callback + fastify.get('/test/:id', function foo(req, res) { + res.send('OK'); + }); + fastify.get('/test-error', () => { + throw Error('foo'); + }); + } + + app.register(subsystem); + + await startServer(); + + await httpRequest.get(`http://localhost:${PORT}/test/1`); + assert.strictEqual(getSpans().length, 4); + }); + + it('should change name for parent http route', async () => { + const spans = memoryExporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 6); + const changedRootSpan = spans[2]; + const span = spans[4]; + assert.strictEqual(changedRootSpan.name, 'GET /test/:id'); + assert.strictEqual(span.name, 'request handler - foo'); + assert.strictEqual(span.parentSpanId, spans[3].spanContext().spanId); + }); + + it('should create span for fastify express runConnect', async () => { + const spans = memoryExporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 6); + const baseSpan = spans[0]; + const span = spans[1]; + assert.strictEqual(span.name, 'middleware - runConnect'); + assert.deepStrictEqual(span.attributes, { + 'fastify.type': 'middleware', + 'plugin.name': 'fastify-express', + 'hook.name': 'onRequest', + }); + + assert.strictEqual(span.parentSpanId, baseSpan.spanContext().spanId); + }); + + it('should create span for fastify express for enhanceRequest', async () => { + const spans = memoryExporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 6); + const baseSpan = spans[2]; + const span = spans[0]; + assert.strictEqual(span.name, 'middleware - enhanceRequest'); + assert.deepStrictEqual(span.attributes, { + 'fastify.type': 'middleware', + 'plugin.name': 'fastify-express', + 'hook.name': 'onRequest', + }); + + assert.strictEqual(span.parentSpanId, baseSpan.spanContext().spanId); + }); + + it('should create span for request', async () => { + const spans = memoryExporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 6); + const baseSpan = spans[3]; + const span = spans[4]; + assert.strictEqual(span.name, 'request handler - foo'); + assert.deepStrictEqual(span.attributes, { + 'plugin.name': 'subsystem', + 'fastify.type': 'request_handler', + 'fastify.name': 'foo', + 'http.route': '/test/:id', + }); + + assert.strictEqual(span.parentSpanId, baseSpan.spanContext().spanId); + }); + + it('should update http.route for http span', async () => { + const spans = memoryExporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 6); + const span = spans[2]; + assert.strictEqual(span.attributes['http.route'], '/test/:id'); + }); + + it('should create span for subsystem anonymous middleware', async () => { + const spans = memoryExporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 6); + const baseSpan = spans[1]; + const span = spans[3]; + assert.strictEqual(span.name, `middleware - ${ANONYMOUS_NAME}`); + assert.deepStrictEqual(span.attributes, { + 'fastify.type': 'middleware', + 'plugin.name': 'subsystem', + 'hook.name': 'onRequest', + }); + + assert.strictEqual(span.parentSpanId, baseSpan.spanContext().spanId); + }); + + it('should update span with error that was raised', async () => { + memoryExporter.reset(); + await httpRequest.get(`http://localhost:${PORT}/test-error`); + const spans = memoryExporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 6); + const span = spans[4]; + assert.strictEqual(span.name, 'request handler - anonymous'); + assert.deepStrictEqual(span.status, { + code: SpanStatusCode.ERROR, + message: 'foo', + }); + assert.deepStrictEqual(span.attributes, { + 'fastify.type': 'request_handler', + 'plugin.name': 'subsystem', + 'http.route': '/test-error', + }); + }); + }); + + describe('spans context', () => { + describe('hook callback', () => { + it('span should end upon done invocation', async () => { + let hookDone: HookHandlerDoneFunction; + const hookExecutedPromise = new Promise(resolve => { + app.addHook( + 'onRequest', + (_req, _reply, done: HookHandlerDoneFunction) => { + hookDone = done; + resolve(); + } + ); + }); + app.get('/test', (_req, reply: FastifyReply) => { + reply.send('request ended in handler'); + }); + await startServer(); + httpRequest.get(`http://localhost:${PORT}/test`); + await hookExecutedPromise; + + // done was not yet called from the hook, so it should not end the span + const preDoneSpans = getSpans().filter( + s => !s.attributes[AttributeNames.PLUGIN_NAME] + ); + assert.strictEqual(preDoneSpans.length, 0); + hookDone!(); + const postDoneSpans = getSpans().filter( + s => !s.attributes[AttributeNames.PLUGIN_NAME] + ); + assert.strictEqual(postDoneSpans.length, 1); + }); + + it('span should end when calling reply.send from hook', async () => { + app.addHook( + 'onRequest', + ( + _req: FastifyRequest, + reply: FastifyReply, + _done: HookHandlerDoneFunction + ) => { + reply.send('request ended prematurely in hook'); + } + ); + app.get('/test', (_req: FastifyRequest, _reply: FastifyReply) => { + throw Error( + 'handler should not be executed as request is ended in onRequest hook' + ); + }); + await startServer(); + await httpRequest.get(`http://localhost:${PORT}/test`); + const spans = getSpans().filter( + s => !s.attributes[AttributeNames.PLUGIN_NAME] + ); + assert.strictEqual(spans.length, 1); + }); + }); + }); + + describe('application hooks', () => { + it('onRoute not instrumented', done => { + app.addHook('onRoute', () => { + assert.strictEqual(context.active(), ROOT_CONTEXT); + }); + // add a route to trigger the 'onRoute' hook + app.get('/test', (_req: FastifyRequest, reply: FastifyReply) => { + reply.send('OK'); + }); + + startServer() + .then(() => done()) + .catch(err => done(err)); + }); + + it('onRegister is not instrumented', done => { + app.addHook('onRegister', () => { + assert.strictEqual(context.active(), ROOT_CONTEXT); + }); + // register a plugin to trigger 'onRegister' hook + app.register((fastify, options, done) => { + done(); + }); + startServer() + .then(() => done()) + .catch(err => done(err)); + }); + + it('onReady is not instrumented', done => { + app.addHook('onReady', () => { + assert.strictEqual(context.active(), ROOT_CONTEXT); + }); + startServer() + .then(() => done()) + .catch(err => done(err)); + }); + + it('onClose is not instrumented', done => { + app.addHook('onClose', () => { + assert.strictEqual(context.active(), ROOT_CONTEXT); + }); + startServer() + .then(() => { + app.close().then(() => done()); + }) + .catch(err => done(err)); + }); + }); + }); +}); diff --git a/plugins/node/opentelemetry-instrumentation-fastify/tsconfig.json b/plugins/node/opentelemetry-instrumentation-fastify/tsconfig.json new file mode 100644 index 0000000000..28be80d266 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +}