Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@fastify/otel

Package Overview
Dependencies
Maintainers
15
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fastify/otel - npm Package Compare versions

Comparing version
0.14.0
to
0.15.0
+40
-9
index.js

@@ -50,2 +50,3 @@ 'use strict'

const kIgnorePaths = Symbol('fastify otel ignore path')
const kRecordExceptions = Symbol('fastify otel record exceptions')

@@ -55,2 +56,3 @@ class FastifyOtelInstrumentation extends InstrumentationBase {

_requestHook = null
_lifecycleHook = null

@@ -61,5 +63,17 @@ constructor (config) {

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
}
if (typeof config?.lifecycleHook === 'function') {
this._lifecycleHook = config.lifecycleHook
}

@@ -364,3 +378,5 @@ if (config?.ignorePaths != null || process.env.OTEL_FASTIFY_IGNORE_PATHS != null) {

})
span.recordException(error)
if (instrumentation[kRecordExceptions] !== false) {
span.recordException(error)
}
}

@@ -464,9 +480,8 @@

const ctx = request[kRequestContext] ?? context.active()
const handlerName = handler.name?.length > 0
? handler.name
: this.pluginName /* c8 ignore next */ ?? ANONYMOUS_FUNCTION_NAME /* c8 ignore next */
const span = instrumentation.tracer.startSpan(
`${hookName} - ${
handler.name?.length > 0
? handler.name
: this.pluginName /* c8 ignore next */ ??
ANONYMOUS_FUNCTION_NAME /* c8 ignore next */
}`,
`${hookName} - ${handlerName}`,
{

@@ -478,2 +493,14 @@ attributes: spanAttributes

if (instrumentation._lifecycleHook != null) {
try {
instrumentation._lifecycleHook(span, {
hookName,
request,
handler: handlerName
})
} catch (err) {
instrumentation.logger.error({ err }, 'Execution of lifecycleHook failed')
}
}
return context.with(

@@ -496,3 +523,5 @@ trace.setSpan(ctx, span),

})
span.recordException(error)
if (instrumentation[kRecordExceptions] !== false) {
span.recordException(error)
}
span.end()

@@ -511,3 +540,5 @@ return Promise.reject(error)

})
span.recordException(error)
if (instrumentation[kRecordExceptions] !== false) {
span.recordException(error)
}
span.end()

@@ -514,0 +545,0 @@ throw error

+1
-1
{
"name": "@fastify/otel",
"version": "0.14.0",
"version": "0.15.0",
"description": "Official Fastify OpenTelemetry Instrumentation",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -222,4 +222,32 @@ # @fastify/otel

#### `FastifyOtelInstrumentationOptions#lifecycleHook: function`
A **synchronous** callback that runs whenever a span is created for a Fastify lifecycle hook (route hooks, instance hooks, not-found handlers, and route handlers).
* **span** – the hook span that was just created
* **info.hookName** – Fastify lifecycle stage (e.g., `onRequest`, `preHandler`, `handler`)
* **info.handler** – the resolved handler or plugin name when available
* **info.request** – the current `FastifyRequest`
Use it to rename hook spans or annotate them with framework-specific metadata (for example, tRPC procedure names) without registering additional Fastify hooks.
```js
const otel = new FastifyOtelInstrumentation({
lifecycleHook: (span, info) => {
if (info.hookName === 'handler' && info.request.headers['x-trpc-op'] != null) {
span.updateName(`tRPC handler - ${info.request.headers['x-trpc-op']}`)
}
span.setAttribute('hook.handler.name', info.handler ?? 'anonymous')
}
})
```
#### `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).

@@ -52,2 +52,8 @@ 'use strict'

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 => {

@@ -277,2 +283,146 @@ const app = Fastify()

})
test('FastifyInstrumentation#lifecycleHook should be invoked for every hook span', async () => {
const app = Fastify()
const calls = []
const exporter = new InMemorySpanExporter()
const provider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(exporter)]
})
provider.register()
const instrumentation = new FastifyInstrumentation({
lifecycleHook: (span, info) => {
calls.push({ hookName: info.hookName, handler: info.handler })
span.updateName(`custom:${info.hookName}`)
span.setAttribute('hook.handler', info.handler ?? 'unknown')
span.addEvent('customized')
}
})
instrumentation.setTracerProvider(provider)
await app.register(instrumentation.plugin())
app.get('/', {
preHandler: function guard (request, reply, done) {
done()
}
}, function routeHandler () {
return 'ok'
})
const res = await app.inject({
method: 'GET',
url: '/'
})
assert.equal(res.statusCode, 200)
assert.equal(res.payload, 'ok')
assert.deepStrictEqual(calls.map(call => call.hookName), ['preHandler', 'handler'])
assert.deepStrictEqual(calls.map(call => call.handler), ['guard', 'routeHandler'])
const hookSpans = exporter.getFinishedSpans().filter(span => span.name.startsWith('custom:'))
assert.equal(hookSpans.length, 2)
assert.ok(hookSpans.every(span => span.attributes['hook.handler'] != null))
})
test('FastifyInstrumentation#lifecycleHook should not crash when it throws', async () => {
const app = Fastify()
const instrumentation = new FastifyInstrumentation({
lifecycleHook: () => {
throw new Error('boom')
}
})
await app.register(instrumentation.plugin())
app.get('/', () => 'ok')
const res = await app.inject({
method: 'GET',
url: '/'
})
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()
})
})

@@ -8,2 +8,3 @@ /// <reference types="node" />

FastifyOtelInstrumentationOpts,
FastifyOtelLifecycleHookInfo,
FastifyOtelOptions,

@@ -31,3 +32,3 @@ FastifyOtelRequestContext

declare namespace exported {
export type { FastifyOtelInstrumentationOpts }
export type { FastifyOtelInstrumentationOpts, FastifyOtelLifecycleHookInfo }
export { FastifyOtelInstrumentation }

@@ -34,0 +35,0 @@ export { FastifyOtelInstrumentation as default }

@@ -16,3 +16,10 @@ import { expectAssignable } from 'tsd'

expectAssignable<FastifyRequest>(request)
}
},
lifecycleHook (span, info) {
expectAssignable<Span>(span)
expectAssignable<string>(info.hookName)
expectAssignable<FastifyRequest>(info.request)
expectAssignable<string | undefined>(info.handler)
},
recordExceptions: false
} as FastifyOtelInstrumentationOpts)

@@ -19,0 +26,0 @@ expectAssignable<InstrumentationConfig>({} as FastifyOtelInstrumentationOpts)

@@ -11,4 +11,12 @@ // types.d.ts

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 {
hookName: string
request: import('fastify').FastifyRequest
handler?: string
}
interface FastifyOtelRequestInfo {

@@ -15,0 +23,0 @@ tracer: Tracer,