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 = { } ) { super ( '@opentelemetry/instrumentation-fastify' , VERSION , Object . assign ( { } , config ) ) ; } init ( ) { return [ new InstrumentationNodeModuleDefinition < any > ( '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 , reply , done ) { 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 , hookName , original , syncFunctionWithDone ) { const instrumentation = this ; return function ( this , ... args ) : Promise < unknown > { 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 ) { endSpan ( reply ) ; origDone . apply ( this , doneArgs ) ; } ; } return context . with ( trace . setSpan ( context . active ( ) , span ) , ( ) => { return safeExecuteInTheMiddleMaybePromise ( ( ) => { return original . apply ( this , args ) ; } , err => { if ( err instanceof Error ) { span . setStatus ( { code : SpanStatusCode . ERROR , message : err . message , } ) ; span . recordException ( err ) ; } if ( ! syncFunctionWithDone ) { endSpan ( reply ) ; } } ) ; } ) ; } ; } private _wrapAddHook ( ) { const instrumentation = this ; return function ( original ) : ( ) => FastifyInstance { return function wrappedAddHook ( this , ... args ) { 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 ) { const instrumentation = this ; function fastify ( this , ... args ) { const app = 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 { return function send ( this , ... args ) { const maybeError = args [ 0 ] ; if ( ! instrumentation . isEnabled ( ) ) { return original . apply ( this , args ) ; } return safeExecuteInTheMiddle < FastifyReply > ( ( ) => { 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 , request , reply , done ) { 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 = { [ 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 ( ) ; } ) ; } ; } }