From 045b51b6512238752ad4b1039087e5e587a88ed5 Mon Sep 17 00:00:00 2001 From: pharrelanokyeadisi-oss Date: Fri, 28 Aug 2026 09:45:20 -0700 Subject: [PATCH] feat: allow disabling handler instrumentation Signed-off-by: pharrelanokyeadisi-oss --- README.md | 10 ++++---- index.js | 7 +++--- test/api.test.js | 1 + test/index.test.js | 62 ++++++++++++++++++++++++++++++++++++++++------ types/types.d.ts | 1 + 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index bfb0646..28c1eb4 100644 --- a/README.md +++ b/README.md @@ -244,15 +244,15 @@ const otel = new FastifyOtelInstrumentation({ Control which Fastify lifecycle hooks receive child spans. Defaults to instrumenting all hooks listed in Usage. -* `true` (default) – instrument all lifecycle hooks (`onRequest`, `preParsing`, `preValidation`, `preHandler`, `preSerialization`, `onSend`, `onResponse`, `onError`) -* `false` – no lifecycle hook child spans (the root `request` span and route `handler` span are still created) -* `string[]` – allowlist of hook names to instrument (for example `['preHandler']`) +* `true` (default) – instrument the route `handler` and all lifecycle hooks (`onRequest`, `preParsing`, `preValidation`, `preHandler`, `preSerialization`, `onSend`, `onResponse`, `onError`) +* `false` – no handler or lifecycle hook child spans (the root `request` span is still created) +* `string[]` – allowlist of hook names to instrument (for example `['handler', 'preHandler']`) Per-route override via `config.otel`: * `otel: false` – disable all OpenTelemetry spans for the route (unchanged) -* `otel: { instrumentHooks: false }` – request and handler spans only -* `otel: { instrumentHooks: true }` – all lifecycle hooks on the route (overrides a global `false`) +* `otel: { instrumentHooks: false }` – request span only +* `otel: { instrumentHooks: true }` – handler and all lifecycle hooks on the route (overrides a global `false`) * `otel: { instrumentHooks: ['preHandler'] }` – allowlist for the route (overrides global settings) Precedence: `otel: false`, then route `otel.instrumentHooks` when set, otherwise the global `instrumentHooks` option. diff --git a/index.js b/index.js index f690b2b..47b0123 100644 --- a/index.js +++ b/index.js @@ -27,6 +27,7 @@ const FASTIFY_HOOKS = [ 'onResponse', 'onError' ] +const INSTRUMENTABLE_HOOKS = [...FASTIFY_HOOKS, 'handler'] const ATTRIBUTE_NAMES = { HOOK_NAME: 'hook.name', FASTIFY_TYPE: 'fastify.type', @@ -77,7 +78,7 @@ function normalizeInstrumentHooks (value, { strict = false, logger = null } = {} const allowlist = new Set() for (const hookName of value) { - if (typeof hookName !== 'string' || !FASTIFY_HOOKS.includes(hookName)) { + if (typeof hookName !== 'string' || !INSTRUMENTABLE_HOOKS.includes(hookName)) { if (strict) { throw new TypeError('instrumentHooks must be a boolean or an array of hook names') } @@ -105,12 +106,12 @@ function getHookPolicy (config, globalPolicy, logger = null) { } function lifecycleHookBaseName (hookName) { - if (FASTIFY_HOOKS.includes(hookName)) { + if (INSTRUMENTABLE_HOOKS.includes(hookName)) { return hookName } if (hookName.includes(' - ')) { const base = hookName.split(' - ').pop() - if (FASTIFY_HOOKS.includes(base)) { + if (INSTRUMENTABLE_HOOKS.includes(base)) { return base } } diff --git a/test/api.test.js b/test/api.test.js index cad67e2..25ca577 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -61,6 +61,7 @@ describe('Interface', () => { assert.throws(() => new FastifyInstrumentation({ instrumentHooks: ['notAHook'] }), /boolean or an array/) assert.doesNotThrow(() => new FastifyInstrumentation({ instrumentHooks: false })) assert.doesNotThrow(() => new FastifyInstrumentation({ instrumentHooks: true })) + assert.doesNotThrow(() => new FastifyInstrumentation({ instrumentHooks: ['handler'] })) assert.doesNotThrow(() => new FastifyInstrumentation({ instrumentHooks: ['preHandler'] })) }) diff --git a/test/index.test.js b/test/index.test.js index d2a9b18..a9f9bd5 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -712,9 +712,9 @@ describe('FastifyInstrumentation', () => { .getFinishedSpans() .filter(span => span.instrumentationScope.name === '@fastify/otel') - assert.equal(spans.length, 2) + assert.equal(spans.length, 1) assert.equal(spans.find(s => s.name === 'request') != null, true) - assert.equal(spans.find(s => s.name === 'handler - helloworld') != null, true) + assert.equal(spans.find(s => s.name === 'handler - helloworld'), undefined) assert.equal(spans.find(s => s.name.startsWith('onRequest -')), undefined) assert.equal(spans.find(s => s.name.startsWith('preHandler -')), undefined) assert.equal(response.status, 200) @@ -750,7 +750,8 @@ describe('FastifyInstrumentation', () => { .getFinishedSpans() .filter(span => span.instrumentationScope.name === '@fastify/otel') - assert.equal(spans.length, 2) + assert.equal(spans.length, 1) + assert.equal(spans.find(s => s.name === 'handler - helloworld'), undefined) assert.equal(spans.find(s => s.name.startsWith('onRequest -')), undefined) assert.equal(spans.find(s => s.name.startsWith('preHandler -')), undefined) assert.equal(response.status, 200) @@ -789,9 +790,51 @@ describe('FastifyInstrumentation', () => { .getFinishedSpans() .filter(span => span.instrumentationScope.name === '@fastify/otel') - assert.equal(spans.length, 3) + assert.equal(spans.length, 2) assert.equal(spans.find(s => s.name === 'preHandler - somePreHandler') != null, true) + assert.equal(spans.find(s => s.name === 'handler - helloworld'), undefined) + assert.equal(spans.find(s => s.name.startsWith('onRequest -')), undefined) + assert.equal(response.status, 200) + }) + + test('should create only the handler span when instrumentHooks allows handler', async t => { + const localInstrumentation = new FastifyInstrumentation({ + instrumentHooks: ['handler'] + }) + localInstrumentation.setTracerProvider(provider) + const app = Fastify() + const plugin = localInstrumentation.plugin() + + await app.register(plugin) + + app.get( + '/', + { + onRequest: (request, reply, done) => { done() }, + preHandler: (request, reply, done) => { done() } + }, + async function helloworld () { + return 'hello world' + } + ) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationScope.name === '@fastify/otel') + + assert.equal(spans.length, 2) + assert.equal(spans.find(s => s.name === 'request') != null, true) + assert.equal(spans.find(s => s.name === 'handler - helloworld') != null, true) assert.equal(spans.find(s => s.name.startsWith('onRequest -')), undefined) + assert.equal(spans.find(s => s.name.startsWith('preHandler -')), undefined) assert.equal(response.status, 200) }) @@ -822,7 +865,7 @@ describe('FastifyInstrumentation', () => { .getFinishedSpans() .filter(span => span.instrumentationScope.name === '@fastify/otel') - assert.equal(spans.length, 2) + assert.equal(spans.length, 1) }) test('should ignore invalid per-route instrumentHooks allowlist entries', async t => { @@ -852,7 +895,8 @@ describe('FastifyInstrumentation', () => { .getFinishedSpans() .filter(span => span.instrumentationScope.name === '@fastify/otel') - assert.equal(spans.length, 2) + assert.equal(spans.length, 1) + assert.equal(spans.find(s => s.name === 'handler - helloworld'), undefined) assert.equal(spans.find(s => s.name.startsWith('onRequest -')), undefined) }) @@ -889,9 +933,10 @@ describe('FastifyInstrumentation', () => { .getFinishedSpans() .filter(span => span.instrumentationScope.name === '@fastify/otel') - assert.equal(spans.length, 3) + assert.equal(spans.length, 2) assert.equal(spans.find(s => s.name.startsWith('onRequest -')), undefined) assert.equal(spans.find(s => s.name === 'preHandler - routePreHandler') != null, true) + assert.equal(spans.find(s => s.name === 'handler - helloworld'), undefined) }) test('should override global instrumentHooks false with per-route allowlist', async t => { @@ -926,8 +971,9 @@ describe('FastifyInstrumentation', () => { .getFinishedSpans() .filter(span => span.instrumentationScope.name === '@fastify/otel') - assert.equal(spans.length, 3) + assert.equal(spans.length, 2) assert.equal(spans.find(s => s.name === 'onRequest - routeOnRequest') != null, true) + assert.equal(spans.find(s => s.name === 'handler - helloworld'), undefined) assert.equal(spans.find(s => s.name.startsWith('preHandler -')), undefined) assert.equal(response.status, 200) }) diff --git a/types/types.d.ts b/types/types.d.ts index de516e8..0462189 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -4,6 +4,7 @@ import type { Context, Span, TextMapGetter, TextMapSetter, Tracer } from '@opent import type { HTTPMethods } from 'fastify' export type FastifyOtelHookName = + | 'handler' | 'onRequest' | 'preParsing' | 'preValidation'