Skip to content

feat(#44): align APIs with previous instrumentation #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 14, 2025
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
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const FastifyOtelInstrumentation = require('@fastify/otel');

// If serverName is not provided, it will fallback to OTEL_SERVICE_NAME
// as per https://opentelemetry.io/docs/languages/sdk-configuration/general/.
const fastifyOtelInstrumentation = new FastifyOtelInstrumentation({ servername: '<yourCustomApplicationName>' });
const fastifyOtelInstrumentation = new FastifyOtelInstrumentation({ servername: '<yourCustomApplicationName>' });
fastifyOtelInstrumentation.setTracerProvider(provider)

module.exports = { fastifyOtelInstrumentation }
Expand Down Expand Up @@ -72,9 +72,12 @@ app.register((instance, opts, done) => {

The plugin can be automatically registered with `registerOnInitialization` option set to `true`.
In this case, it is necessary to await fastify instance.

```js
// ... in your OTEL setup
const fastifyOtelInstrumentation = new FastifyOtelInstrumentation({ registerOnInitialization: true });
const fastifyOtelInstrumentation = new FastifyOtelInstrumentation({
registerOnInitialization: true,
});

// ... in your Fastify definition
const Fastify = require('fastify');
Expand All @@ -88,6 +91,42 @@ const app = await fastify();

For more information about OpenTelemetry, please refer to the [OpenTelemetry JavaScript](https://opentelemetry.io/docs/languages/js/) documentation.

## APIs

### 'FastifyOtelRequestContext`

The `FastifyOtelRequestContext` is a wrapper around the OpenTelemetry `Context` and `Tracer` APIs. It also provides a way to manage the context of a request and its associated spans as well as some utilities to extract and inject further traces from and to the trace carrier.

```js
const { fastifyOtelInstrumentation } = require('./otel.js');
const Fastify = require('fastify');

const app = fastify();
await app.register(fastifyOtelInstrumentation.plugin());

app.get('/', (req, reply) => {
const { context, tracer, span, inject, extract } = req.opentelemetry();

// Extract a parent span from the request headers
const parentCxt = extract(req.headers);

// Create a new span
const newSpan = tracer.startSpan('my-new-span', {
parent: parentCxt,
});
// Do some work
newSpan.end();

// Inject the new span into the response headers
const carrier = {};
inject(carrier);

reply.headers(carrier);

return 'hello world';
});
```

## License

Licensed under [MIT](./LICENSE).
53 changes: 14 additions & 39 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,33 @@
/// <reference types="node" />

import { Context, Span, TextMapGetter, TextMapSetter, Tracer } from '@opentelemetry/api'
import { InstrumentationBase, InstrumentationConfig, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'

interface FastifyReply {
send: () => FastifyReply;
statusCode: number;
}

interface FastifyRequest {
method?: string;
// since [email protected]
routeOptions?: {
url?: string;
};
routerPath?: string;
}

type HandlerOriginal =
| ((request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => Promise<void>)
| ((request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => void)

type FastifyError = any

type HookHandlerDoneFunction = <TError extends Error = FastifyError>(err?: TError) => void

export type FastifyPlugin = (
instance: FastifyInstance,
opts: any,
done: HookHandlerDoneFunction,
) => unknown | Promise<unknown>
import { FastifyPluginCallback } from 'fastify'

export interface FastifyOtelOptions {}
export interface FastifyOtelInstrumentationOpts extends InstrumentationConfig {
servername?: string
registerOnInitialization?: boolean
}
export type FastifyOtelRequestContext = {
span: Span,
tracer: Tracer,
context: Context,
inject: (carrier: {}, setter?: TextMapSetter) => void;
extract: (carrier: {}, getter?: TextMapGetter) => Context
}

export interface FastifyInstance {
version: string;
register: (plugin: any) => FastifyInstance;
after: (listener?: (err: Error) => void) => FastifyInstance;
addHook(hook: string, handler: HandlerOriginal): FastifyInstance;
addHook(
hook: 'onError',
handler: (request: FastifyRequest, reply: FastifyReply, error: Error) => void,
): FastifyInstance;
addHook(hook: 'onRequest', handler: (request: FastifyRequest, reply: FastifyReply) => void): FastifyInstance;
declare module 'fastify' {
interface FastifyRequest {
opentelemetry(): FastifyOtelRequestContext
}
}

declare class FastifyOtelInstrumentation<Config extends FastifyOtelInstrumentationOpts = FastifyOtelInstrumentationOpts> extends InstrumentationBase<Config> {
static FastifyInstrumentation: FastifyOtelInstrumentation
constructor (config?: FastifyOtelInstrumentationOpts)
init (): InstrumentationNodeModuleDefinition[]
plugin (): FastifyPlugin
plugin (): FastifyPluginCallback<FastifyOtelOptions>
}

export default FastifyOtelInstrumentation
Expand Down
15 changes: 15 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ class FastifyOtelInstrumentation extends InstrumentationBase {
// what is important is to bound it to the right instance
instance.decorate(kAddHookOriginal, instance.addHook)
instance.decorate(kSetNotFoundOriginal, instance.setNotFoundHandler)
instance.decorateRequest('opentelemetry', function openetelemetry () {
const ctx = this[kRequestContext]
const span = this[kRequestSpan]
return {
span,
tracer: instrumentation.tracer,
context: ctx,
inject: (carrier, setter) => {
return propagation.inject(ctx, carrier, setter)
},
extract: (carrier, getter) => {
return propagation.extract(ctx, carrier, getter)
}
}
})
instance.decorateRequest(kRequestSpan, null)
instance.decorateRequest(kRequestContext, null)

Expand Down
32 changes: 31 additions & 1 deletion test/api.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const { test, describe } = require('node:test')
const assert = require('assert')
const assert = require('node:assert')
const Fastify = require(process.env.FASTIFY_VERSION || 'fastify')

const { InstrumentationBase } = require('@opentelemetry/instrumentation')
Expand Down Expand Up @@ -38,4 +38,34 @@ describe('Interface', () => {

await app.ready()
})

test('FastifyInstrumentation#plugin should expose the right set of APIs', async t => {
/** @type {import('fastify').FastifyInstance} */
const app = Fastify()
const instrumentation = new FastifyInstrumentation()
const plugin = instrumentation.plugin()

await app.register(plugin)

app.get('/', (request, reply) => {
const otel = request.opentelemetry()

assert.equal(typeof otel.span.spanContext().spanId, 'string')
assert.equal(typeof otel.tracer, 'object')
assert.equal(typeof otel.context, 'object')
assert.equal(typeof otel.inject, 'function')
assert.equal(otel.inject.length, 2)
assert.ok(!otel.inject({}))
assert.equal(typeof otel.extract, 'function')
assert.equal(otel.extract.length, 2)
assert.equal(typeof (otel.extract({})), 'object')

return 'world'
})

await app.inject({
method: 'GET',
url: '/'
})
})
})
16 changes: 13 additions & 3 deletions test/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { expectAssignable } from 'tsd'
import { InstrumentationBase, InstrumentationConfig } from '@opentelemetry/instrumentation'
import { fastify as Fastify } from 'fastify'
import { Context, Span, TextMapGetter, TextMapSetter, Tracer } from '@opentelemetry/api'
import { fastify as Fastify, FastifyInstance, FastifyPluginCallback } from 'fastify'

import { FastifyOtelInstrumentation, FastifyOtelInstrumentationOpts, FastifyInstance, FastifyPlugin } from '..'
import { FastifyOtelInstrumentation, FastifyOtelInstrumentationOpts } from '..'

expectAssignable<InstrumentationBase>(new FastifyOtelInstrumentation())
expectAssignable<InstrumentationConfig>({ servername: 'server', enabled: true } as FastifyOtelInstrumentationOpts)
Expand All @@ -12,7 +13,7 @@ const app = Fastify()
const plugin = new FastifyOtelInstrumentation().plugin()

expectAssignable<FastifyInstance>(app)
expectAssignable<FastifyPlugin>(plugin)
expectAssignable<FastifyPluginCallback>(plugin)
expectAssignable<FastifyInstance>(app.register(plugin))
expectAssignable<FastifyInstance>(app.register(plugin).register(plugin))

Expand All @@ -21,3 +22,12 @@ app.register((nested, _opts, done) => {
nested.register(new FastifyOtelInstrumentation().plugin())
done()
})

app.get('/', async function (request, reply) {
const otel = request.opentelemetry()
expectAssignable<Span>(otel.span)
expectAssignable<Context>(otel.context)
expectAssignable<Tracer>(otel.tracer)
expectAssignable<(carrier: any, setter?: TextMapSetter) => void>(otel.inject)
expectAssignable<(carrier: any, getter?: TextMapGetter) => Context>(otel.extract)
})