import { hrTime , setRPCMetadata , getRPCMetadata , RPCType , } from '@opentelemetry/core' ; import { trace , context , diag , SpanAttributes } from '@opentelemetry/api' ; import * as express from 'express' ; import { ExpressLayer , ExpressRouter , PatchedRequest , _LAYERS_STORE_PROPERTY , ExpressInstrumentationConfig , } from './types' ; import { ExpressLayerType } from './enums/ExpressLayerType' ; import { AttributeNames } from './enums/AttributeNames' ; import { getLayerMetadata , storeLayerPath , isLayerIgnored } from './utils' ; import { VERSION } from './version' ; import { InstrumentationBase , InstrumentationNodeModuleDefinition , isWrapped , } from '@opentelemetry/instrumentation' ; import { SemanticAttributes } from '@opentelemetry/semantic-conventions' ; /**
 * This symbol is used to mark express layer as being already instrumented
 * since its possible to use a given layer multiple times (ex: middlewares)
 */ export const kLayerPatched = Symbol ( 'express-layer-patched' ) ; /** Express instrumentation for OpenTelemetry */ export class ExpressInstrumentation extends InstrumentationBase < typeof express > { constructor ( config = { } ) { super ( '@opentelemetry/instrumentation-express' , VERSION , Object . assign ( { } , config ) ) ; } init ( ) { return [ new InstrumentationNodeModuleDefinition < typeof express > ( 'express' , [ '^4.0.0' ] , ( moduleExports , moduleVersion ) => { diag . debug ( `Applying patch for express@${ moduleVersion }` ) ; const routerProto = moduleExports . Router as unknown as express . Router ; if ( isWrapped ( routerProto . route ) ) { this . _unwrap ( routerProto , 'route' ) ; } this . _wrap ( routerProto , 'route' , this . _getRoutePatch ( ) ) ; if ( isWrapped ( routerProto . use ) ) { this . _unwrap ( routerProto , 'use' ) ; } this . _wrap ( routerProto , 'use' , this . _getRouterUsePatch ( ) as any ) ; if ( isWrapped ( moduleExports . application . use ) ) { this . _unwrap ( moduleExports . application , 'use' ) ; } this . _wrap ( moduleExports . application , 'use' , this . _getAppUsePatch ( ) as any ) ; return moduleExports ; } , ( moduleExports , moduleVersion ) => { if ( moduleExports === undefined ) return ; diag . debug ( `Removing patch for express@${ moduleVersion }` ) ; this . _unwrap ( moduleExports . Router . prototype , 'route' ) ; this . _unwrap ( moduleExports . Router . prototype , 'use' ) ; this . _unwrap ( moduleExports . application . prototype , 'use' ) ; } ) , ] ; } /**
     * Get the patch for Router.route function
     */ private _getRoutePatch ( ) { const instrumentation = this ; return function ( original ) { return function route_trace ( this , ... args ) { const route = original . apply ( this , args ) ; const layer = this . stack [ this . stack . length - 1 ] as ExpressLayer ; instrumentation . _applyPatch ( layer , typeof args [ 0 ] === 'string' ? args [ 0 ] : undefined ) ; return route ; } ; } ; } /**
     * Get the patch for Router.use function
     */ private _getRouterUsePatch ( ) { const instrumentation = this ; return function ( original ) { return function use ( this , ... args ) { const route = original . apply ( this , args ) ; const layer = this . stack [ this . stack . length - 1 ] as ExpressLayer ; instrumentation . _applyPatch ( layer , typeof args [ 0 ] === 'string' ? args [ 0 ] : undefined ) ; return route ; } ; } ; } /**
     * Get the patch for Application.use function
     */ private _getAppUsePatch ( ) { const instrumentation = this ; return function ( original ) { return function use ( this , ... args ) { const route = original . apply ( this , args ) ; const layer = this . _router . stack [ this . _router . stack . length - 1 ] ; instrumentation . _applyPatch . call ( instrumentation , layer , typeof args [ 0 ] === 'string' ? args [ 0 ] : undefined ) ; return route ; } ; } ; } /** Patch each express layer to create span and propagate context */ private _applyPatch ( this , layer , layerPath ? ) { const instrumentation = this ; if ( layer [ kLayerPatched ] === true ) return ; layer [ kLayerPatched ] = true ; this . _wrap ( layer , 'handle' , ( original ) => { if ( original . length === 4 ) return original ; return function ( this , req , res ) { storeLayerPath ( req , layerPath ) ; const route = ( req [ _LAYERS_STORE_PROPERTY ] as string [ ] ) . filter ( path => path !== '/' && path !== '/*' ) . join ( '' ) ; const attributes = { [ SemanticAttributes . HTTP_ROUTE ] : route . length > 0 ? route : '/' , } ; const metadata = getLayerMetadata ( layer , layerPath ) ; const type = metadata . attributes [ AttributeNames . EXPRESS_TYPE ] as ExpressLayerType ; const rpcMetadata = getRPCMetadata ( context . active ( ) ) ; if ( metadata . attributes [ AttributeNames . EXPRESS_TYPE ] === ExpressLayerType . REQUEST_HANDLER && rpcMetadata ?. type === RPCType . HTTP ) { rpcMetadata . span . updateName ( `${ req . method } ${ route . length > 0 ? route : '/' }` ) ; } if ( isLayerIgnored ( metadata . name , type , instrumentation . _config ) ) { if ( type === ExpressLayerType . MIDDLEWARE ) { ( req [ _LAYERS_STORE_PROPERTY ] as string [ ] ) . pop ( ) ; } return original . apply ( this , arguments ) ; } if ( trace . getSpan ( context . active ( ) ) === undefined ) { return original . apply ( this , arguments ) ; } const span = instrumentation . tracer . startSpan ( metadata . name , { attributes : Object . assign ( attributes , metadata . attributes ) , } ) ; const startTime = hrTime ( ) ; let spanHasEnded = false ; if ( metadata . attributes [ AttributeNames . EXPRESS_TYPE ] !== ExpressLayerType . MIDDLEWARE ) { span . end ( startTime ) ; spanHasEnded = true ; } const onResponseFinish = ( ) => { if ( spanHasEnded === false ) { spanHasEnded = true ; span . end ( startTime ) ; } } ; const args = Array . from ( arguments ) ; const callbackIdx = args . findIndex ( arg => typeof arg === 'function' ) ; const newContext = rpcMetadata ?. type === RPCType . HTTP ? setRPCMetadata ( context . active ( ) , Object . assign ( rpcMetadata , { route : route } ) ) : context . active ( ) ; if ( callbackIdx >= 0 ) { arguments [ callbackIdx ] = function ( ) { if ( spanHasEnded === false ) { spanHasEnded = true ; req . res ?. removeListener ( 'finish' , onResponseFinish ) ; span . end ( ) ; } if ( ! ( req . route && arguments [ 0 ] instanceof Error ) ) { ( req [ _LAYERS_STORE_PROPERTY ] as string [ ] ) . pop ( ) ; } const callback = args [ callbackIdx ] as Function ; return context . bind ( newContext , callback ) . apply ( this , arguments ) ; } ; } const result = original . apply ( this , arguments ) ; /**
                 * At this point if the callback wasn't called, that means either the
                 * layer is asynchronous (so it will call the callback later on) or that
                 * the layer directly end the http response, so we'll hook into the "finish"
                 * event to handle the later case.
                 */ if ( ! spanHasEnded ) { res . once ( 'finish' , onResponseFinish ) ; } return result ; } ; } ) ; } }