diff --git a/package-lock.json b/package-lock.json index ddc8924..68585f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/http", - "version": "5.59.0", + "version": "5.61.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/http", - "version": "5.59.0", + "version": "5.61.0", "license": "MIT", "devDependencies": { "@athenna/artisan": "^5.11.0", diff --git a/package.json b/package.json index 0cf4b09..e1ddf11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/http", - "version": "5.59.0", + "version": "5.61.0", "description": "The Athenna Http server. Built on top of fastify.", "license": "MIT", "author": "João Lenon ", diff --git a/src/handlers/FastifyHandler.ts b/src/handlers/FastifyHandler.ts index 80eaf09..fc83fec 100644 --- a/src/handlers/FastifyHandler.ts +++ b/src/handlers/FastifyHandler.ts @@ -19,6 +19,7 @@ import { NotFoundException } from '#src/exceptions/NotFoundException' import type { FastifyReply, FastifyRequest, RouteHandlerMethod } from 'fastify' const otelApi = await Module.safeImport('@opentelemetry/api') +const otelCurrentContextBagKey = Symbol.for('athenna.otel.currentContextBag') export class FastifyHandler { /** @@ -132,6 +133,7 @@ export class FastifyHandler { } let otelContext = otelApi.context.active() + const bag = new Map() for (const binding of Config.get('http.otel.contextBindings', [])) { const value = binding.resolve(ctx) @@ -140,9 +142,12 @@ export class FastifyHandler { continue } + bag.set(binding.key, value) otelContext = otelContext.setValue(binding.key, value) } + req.data.otelCurrentContextBag = bag + otelContext = otelContext.setValue(otelCurrentContextBagKey as any, bag) req.otelContext = otelContext return otelContext as OtelContext diff --git a/tests/unit/server/ServerTest.ts b/tests/unit/server/ServerTest.ts index d6e73df..0400f0f 100644 --- a/tests/unit/server/ServerTest.ts +++ b/tests/unit/server/ServerTest.ts @@ -13,6 +13,8 @@ import { context, createContextKey } from '@opentelemetry/api' import { Test, AfterEach, BeforeEach, type Context, Cleanup } from '@athenna/test' import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks' +const otelCurrentContextBagKey = Symbol.for('athenna.otel.currentContextBag') + export default class ServerTest { @BeforeEach() public async beforeEach() { @@ -266,6 +268,44 @@ export default class ServerTest { }) } + @Test() + @Cleanup(() => Config.set('http.otel.contextEnabled', false)) + @Cleanup(() => Config.set('http.otel.contextBindings', [])) + public async shouldCreateAndReuseTheSameRequestContextBagAcrossTheLifecycle({ assert }: Context) { + const exampleIdKey = 'exampleId' + let requestBag: Map = null + let terminateBag: Map = null + + Config.set('http.otel.contextEnabled', true) + Config.set('http.otel.contextBindings', [{ key: exampleIdKey, resolve: () => 'example-id-from-binding' }]) + + Server.terminate(() => { + terminateBag = context.active().getValue(otelCurrentContextBagKey as any) as Map< + string | symbol, + unknown + > + }).get({ + url: '/test', + handler: async ctx => { + requestBag = context.active().getValue(otelCurrentContextBagKey as any) as Map< + string | symbol, + unknown + > + requestBag.set(exampleIdKey, 'example-id-from-handler') + + await ctx.response.send({ + exampleId: requestBag.get(exampleIdKey) + }) + } + }) + + const response = await Server.request().get('/test') + + assert.deepEqual(response.json(), { exampleId: 'example-id-from-handler' }) + assert.strictEqual(requestBag, terminateBag) + assert.equal(terminateBag.get(exampleIdKey), 'example-id-from-handler') + } + @Test() @Cleanup(() => Config.set('http.otel.contextEnabled', false)) @Cleanup(() => Config.set('http.otel.contextBindings', [])) @@ -293,4 +333,45 @@ export default class ServerTest { assert.equal(response.statusCode, 500) assert.deepEqual(response.json(), { route: '/boom' }) } + + @Test() + @Cleanup(() => Config.set('http.otel.contextEnabled', false)) + @Cleanup(() => Config.set('http.otel.contextBindings', [])) + public async shouldExposeTheSameRequestContextBagInsideErrorHandlers({ assert }: Context) { + let requestBag: Map = null + let errorBag: Map = null + + Config.set('http.otel.contextEnabled', true) + Config.set('http.otel.contextBindings', [{ key: 'exampleId', resolve: () => 'example-id-from-binding' }]) + + Server.setErrorHandler(async ctx => { + errorBag = context.active().getValue(otelCurrentContextBagKey as any) as Map< + string | symbol, + unknown + > + + await ctx.response.status(500).send({ + exampleId: errorBag.get('exampleId') + }) + }) + + Server.get({ + url: '/boom', + handler: async () => { + requestBag = context.active().getValue(otelCurrentContextBagKey as any) as Map< + string | symbol, + unknown + > + requestBag.set('exampleId', 'example-id-from-handler') + + throw new Error('boom') + } + }) + + const response = await Server.request().get('/boom') + + assert.equal(response.statusCode, 500) + assert.deepEqual(response.json(), { exampleId: 'example-id-from-handler' }) + assert.strictEqual(requestBag, errorBag) + } }