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 ) => { 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 ( ) { const spans = memoryExporter . getFinishedSpans ( ) . filter ( s => { return ( s . instrumentationLibrary . name === '@opentelemetry/instrumentation-fastify' ) ; } ) ; return spans ; } describe ( 'fastify' , ( ) => { let PORT ; let app ; function startServer ( ) { return new Promise < void > ( 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 < void > ( 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 ) ; } ) ; } ) ; 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 ( ) => { 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 ) { fastify . addHook ( 'onRequest' , async ( req , res , next ) => { next ( ) ; } ) ; fastify . use ( ( req , res , next ) => { next ( ) ; } ) ; 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 ; const hookExecutedPromise = new Promise < void > ( resolve => { app . addHook ( 'onRequest' , ( _req , _reply , done ) => { hookDone = done ; resolve ( ) ; } ) ; } ) ; app . get ( '/test' , ( _req , reply ) => { reply . send ( 'request ended in handler' ) ; } ) ; await startServer ( ) ; httpRequest . get ( `http://localhost:${ PORT }/test` ) ; await hookExecutedPromise ; 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 , reply , _done ) => { reply . send ( 'request ended prematurely in hook' ) ; } ) ; app . get ( '/test' , ( _req , _reply ) => { 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 ) ; } ) ; app . get ( '/test' , ( _req , reply ) => { 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 ) ; } ) ; 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 ) ) ; } ) ; } ) ; } ) ; } ) ;