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"
+ ]
+}