Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ const otel = new FastifyOtelInstrumentation({
})
```

#### `FastifyOtelInstrumentationOptions#recordExceptions: boolean`

Control whether the instrumentation automatically calls `span.recordException` when a handler or hook throws.
Defaults to `true`, recording every exception. Set it to `false` if you prefer to record only the
exceptions that you consider actionable (for example to avoid noisy `4xx` entries in Datadog Error Tracking).

## License

Licensed under [MIT](./LICENSE).
22 changes: 19 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const kRequestContext = Symbol('fastify otel request context')
const kAddHookOriginal = Symbol('fastify otel addhook original')
const kSetNotFoundOriginal = Symbol('fastify otel setnotfound original')
const kIgnorePaths = Symbol('fastify otel ignore path')
const kRecordExceptions = Symbol('fastify otel record exceptions')

class FastifyOtelInstrumentation extends InstrumentationBase {
logger = null
Expand All @@ -57,6 +58,15 @@ class FastifyOtelInstrumentation extends InstrumentationBase {
super(PACKAGE_NAME, PACKAGE_VERSION, config)
this.logger = diag.createComponentLogger({ namespace: PACKAGE_NAME })
this[kIgnorePaths] = null
this[kRecordExceptions] = true

if (config?.recordExceptions != null) {
if (typeof config.recordExceptions !== 'boolean') {
throw new TypeError('recordExceptions must be a boolean')
}

this[kRecordExceptions] = config.recordExceptions
}
if (typeof config?.requestHook === 'function') {
this._requestHook = config.requestHook
}
Expand Down Expand Up @@ -362,7 +372,9 @@ class FastifyOtelInstrumentation extends InstrumentationBase {
code: SpanStatusCode.ERROR,
message: error.message
})
span.recordException(error)
if (instrumentation[kRecordExceptions] !== false) {
span.recordException(error)
}
}

hookDone()
Expand Down Expand Up @@ -502,7 +514,9 @@ class FastifyOtelInstrumentation extends InstrumentationBase {
code: SpanStatusCode.ERROR,
message: error.message
})
span.recordException(error)
if (instrumentation[kRecordExceptions] !== false) {
span.recordException(error)
}
span.end()
return Promise.reject(error)
}
Expand All @@ -516,7 +530,9 @@ class FastifyOtelInstrumentation extends InstrumentationBase {
code: SpanStatusCode.ERROR,
message: error.message
})
span.recordException(error)
if (instrumentation[kRecordExceptions] !== false) {
span.recordException(error)
}
span.end()
throw error
}
Expand Down
83 changes: 83 additions & 0 deletions test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ describe('Interface', () => {
assert.doesNotThrow(() => new FastifyInstrumentation({ ignorePaths: '/foo' }))
})

test('FastifyOtelInstrumentationOpts#recordExceptions - should be a boolean when provided', async t => {
assert.throws(() => new FastifyInstrumentation({ recordExceptions: 'nope' }), /boolean/)
assert.doesNotThrow(() => new FastifyInstrumentation({ recordExceptions: true }))
assert.doesNotThrow(() => new FastifyInstrumentation({ recordExceptions: false }))
})

test('NamedFastifyInstrumentation#plugin should return a valid Fastify Plugin', async t => {
const app = Fastify()
const instrumentation = new FastifyOtelInstrumentation()
Expand Down Expand Up @@ -340,4 +346,81 @@ describe('Interface', () => {
assert.equal(res.statusCode, 200)
assert.equal(res.payload, 'ok')
})

test('FastifyInstrumentationOptions#recordExceptions defaults to true', async () => {
const exporter = new InMemorySpanExporter()
const provider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(exporter)]
})
provider.register()

const instrumentation = new FastifyInstrumentation()
instrumentation.setTracerProvider(provider)

/** @type {import('fastify').FastifyInstance} */
const app = Fastify()
await app.register(instrumentation.plugin())

app.get('/', async function badRequest () {
const error = new Error('book not found')
error.statusCode = 404
throw error
})

const res = await app.inject({
method: 'GET',
url: '/'
})

assert.equal(res.statusCode, 404)

const spans = exporter.getFinishedSpans()
const handlerSpan = spans.find((span) => span.name.startsWith('handler'))

assert.ok(handlerSpan)
assert.equal(handlerSpan.events.length, 1)
assert.equal(handlerSpan.events[0].name, 'exception')

await app.close()
instrumentation.disable()
})

test('FastifyInstrumentationOptions#recordExceptions can be disabled', async () => {
const exporter = new InMemorySpanExporter()
const provider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(exporter)]
})
provider.register()

const instrumentation = new FastifyInstrumentation({
recordExceptions: false
})
instrumentation.setTracerProvider(provider)

/** @type {import('fastify').FastifyInstance} */
const app = Fastify()
await app.register(instrumentation.plugin())

app.get('/', async function badRequest () {
const error = new Error('book not found')
error.statusCode = 404
throw error
})

const res = await app.inject({
method: 'GET',
url: '/'
})

assert.equal(res.statusCode, 404)

const spans = exporter.getFinishedSpans()
const handlerSpan = spans.find((span) => span.name.startsWith('handler'))

assert.ok(handlerSpan)
assert.equal(handlerSpan.events.length, 0)

await app.close()
instrumentation.disable()
})
})
3 changes: 2 additions & 1 deletion types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ expectAssignable<InstrumentationConfig>({
expectAssignable<string>(info.hookName)
expectAssignable<FastifyRequest>(info.request)
expectAssignable<string | undefined>(info.handler)
}
},
recordExceptions: false
} as FastifyOtelInstrumentationOpts)
expectAssignable<InstrumentationConfig>({} as FastifyOtelInstrumentationOpts)

Expand Down
1 change: 1 addition & 0 deletions types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface FastifyOtelInstrumentationOpts extends InstrumentationConfig {
ignorePaths?: string | ((routeOpts: { url: string, method: HTTPMethods }) => boolean);
requestHook?: (span: import('@opentelemetry/api').Span, request: import('fastify').FastifyRequest) => void
lifecycleHook?: (span: import('@opentelemetry/api').Span, info: FastifyOtelLifecycleHookInfo) => void
recordExceptions?: boolean
}

export interface FastifyOtelLifecycleHookInfo {
Expand Down