From c9923e3636649c67e5122531f164909b48dbb58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luismi=20Ram=C3=ADrez?= Date: Sun, 6 Nov 2022 11:01:31 +0100 Subject: [PATCH] feat(fastify): add requestHook support (#1255) --- .../README.md | 22 ++++++ .../package.json | 2 +- .../src/index.ts | 1 + .../src/instrumentation.ts | 25 ++++++- .../src/types.ts | 39 +++++++++++ .../test/instrumentation.test.ts | 68 +++++++++++++++++++ 6 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 plugins/node/opentelemetry-instrumentation-fastify/src/types.ts diff --git a/plugins/node/opentelemetry-instrumentation-fastify/README.md b/plugins/node/opentelemetry-instrumentation-fastify/README.md index 89c4a6e1d05..3e74f96b8b8 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/README.md +++ b/plugins/node/opentelemetry-instrumentation-fastify/README.md @@ -45,6 +45,28 @@ registerInstrumentations({ See [examples/fastify](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/examples/fastify) for a short example. +## Fastify Instrumentation Options + +| Options | Type | Example | Description | +| `requestHook` | `FastifyCustomAttributeFunction` | `(span, requestInfo) => {}` | Function for adding custom attributes to Fastify requests. Receives params: `Span, FastifyRequestInfo`. | + +### Using `requestHook` + +Instrumentation configuration accepts a custom "hook" function which will be called for every instrumented Fastify request. Custom attributes can be set on the span or run any custom logic per request. + +```javascript +import { FastifyInstrumentation } from "@opentelemetry/instrumentation-fastify" + +const fastifyInstrumentation = new FastifyInstrumentation({ + requestHook: function (span: Span, info: FastifyRequestInfo) { + span.setAttribute( + 'http.method', + info.request.method, + ) + } +}); +``` + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/plugins/node/opentelemetry-instrumentation-fastify/package.json b/plugins/node/opentelemetry-instrumentation-fastify/package.json index 915fcda5425..71b0a1b8430 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/package.json +++ b/plugins/node/opentelemetry-instrumentation-fastify/package.json @@ -54,8 +54,8 @@ "@types/express": "4.17.13", "@types/mocha": "7.0.2", "@types/node": "18.11.7", - "gts": "3.1.0", "fastify": "^4.5.3", + "gts": "3.1.0", "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/index.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/index.ts index 34b600dd0c4..958c8fef607 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/src/index.ts +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/index.ts @@ -15,4 +15,5 @@ */ export * from './enums/AttributeNames'; +export * from './types'; export * from './instrumentation'; diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts index 4f6df78268c..94b4feb0357 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts @@ -23,7 +23,6 @@ import { import { getRPCMetadata, RPCType } from '@opentelemetry/core'; import { InstrumentationBase, - InstrumentationConfig, InstrumentationNodeModuleDefinition, safeExecuteInTheMiddle, } from '@opentelemetry/instrumentation'; @@ -39,6 +38,7 @@ import { FastifyTypes, } from './enums/AttributeNames'; import type { HandlerOriginal, PluginFastifyReply } from './internal-types'; +import type { FastifyInstrumentationConfig } from './types'; import { endSpan, safeExecuteInTheMiddleMaybePromise, @@ -50,7 +50,7 @@ export const ANONYMOUS_NAME = 'anonymous'; /** Fastify instrumentation for OpenTelemetry */ export class FastifyInstrumentation extends InstrumentationBase { - constructor(config: InstrumentationConfig = {}) { + constructor(config: FastifyInstrumentationConfig = {}) { super( '@opentelemetry/instrumentation-fastify', VERSION, @@ -58,6 +58,14 @@ export class FastifyInstrumentation extends InstrumentationBase { ); } + override setConfig(config: FastifyInstrumentationConfig = {}) { + this._config = Object.assign({}, config); + } + + override getConfig(): FastifyInstrumentationConfig { + return this._config as FastifyInstrumentationConfig; + } + init() { return [ new InstrumentationNodeModuleDefinition( @@ -271,6 +279,19 @@ export class FastifyInstrumentation extends InstrumentationBase { spanName, spanAttributes ); + + if (instrumentation.getConfig().requestHook) { + safeExecuteInTheMiddle( + () => instrumentation.getConfig().requestHook!(span, { request }), + e => { + if (e) { + instrumentation._diag.error('request hook failed', e); + } + }, + true + ); + } + 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 00000000000..2deadc05988 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/types.ts @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Span } from '@opentelemetry/api'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export interface FastifyRequestInfo { + request: any; // FastifyRequest object from fastify package +} + +/** + * Function that can be used to add custom attributes to the current span + * @param span - The Fastify handler span. + * @param info - The Fastify request info object. + */ +export interface FastifyCustomAttributeFunction { + (span: Span, info: FastifyRequestInfo): void; +} + +/** + * Options available for the Fastify Instrumentation + */ +export interface FastifyInstrumentationConfig extends InstrumentationConfig { + /** Function for adding custom attributes to each handler span */ + requestHook?: FastifyCustomAttributeFunction; +} diff --git a/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts b/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts index aa17f3b246b..d80595728a0 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts +++ b/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts @@ -24,6 +24,7 @@ import { ReadableSpan, SimpleSpanProcessor, } from '@opentelemetry/sdk-trace-base'; +import { Span } from '@opentelemetry/api'; import { HookHandlerDoneFunction } from 'fastify/types/hooks'; import { FastifyReply } from 'fastify/types/reply'; import { FastifyRequest } from 'fastify/types/request'; @@ -31,6 +32,7 @@ import * as http from 'http'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { ANONYMOUS_NAME } from '../src/instrumentation'; import { AttributeNames, FastifyInstrumentation } from '../src'; +import { FastifyRequestInfo } from '../src/types'; const URL = require('url').URL; @@ -426,5 +428,71 @@ describe('fastify', () => { await startServer(); }); }); + + describe('using requestHook in config', () => { + it('calls requestHook provided function when set in config', async () => { + const requestHook = (span: Span, info: FastifyRequestInfo) => { + span.setAttribute( + SemanticAttributes.HTTP_METHOD, + info.request.method + ); + }; + + instrumentation.setConfig({ + ...instrumentation.getConfig(), + requestHook, + }); + + 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 -> @fastify/express', + [SemanticAttributes.HTTP_ROUTE]: '/test', + [SemanticAttributes.HTTP_METHOD]: 'GET', + }); + }); + + it('does not propagate an error from a requestHook that throws exception', async () => { + const requestHook = (span: Span, info: FastifyRequestInfo) => { + span.setAttribute( + SemanticAttributes.HTTP_METHOD, + info.request.method + ); + + throw Error('error thrown in requestHook'); + }; + + instrumentation.setConfig({ + ...instrumentation.getConfig(), + requestHook, + }); + + 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 -> @fastify/express', + [SemanticAttributes.HTTP_ROUTE]: '/test', + [SemanticAttributes.HTTP_METHOD]: 'GET', + }); + }); + }); }); });