Skip to content

Commit d429cdd

Browse files
committed
Realign spans between versions
1 parent 8ff5f09 commit d429cdd

File tree

8 files changed

+392
-66
lines changed

8 files changed

+392
-66
lines changed

dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,36 +53,40 @@ test('Sends an API route transaction', async ({ baseURL }) => {
5353
span_id: expect.stringMatching(/[a-f0-9]{16}/),
5454
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
5555
data: {
56-
'sentry.origin': 'manual',
56+
'sentry.origin': 'auto.http.otel.fastify',
57+
'sentry.op': 'hook.fastify',
5758
'service.name': 'fastify',
5859
'hook.name': 'fastify -> @fastify/otel -> @fastify/middie - onRequest',
5960
'fastify.type': 'hook',
6061
'hook.callback.name': 'runMiddie',
6162
},
62-
description: 'handler - runMiddie',
63+
description: '@fastify/middie - onRequest',
64+
op: 'hook.fastify',
6365
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
6466
start_timestamp: expect.any(Number),
6567
timestamp: expect.any(Number),
6668
status: 'ok',
67-
origin: 'manual',
69+
origin: 'auto.http.otel.fastify',
6870
},
6971
{
7072
span_id: expect.stringMatching(/[a-f0-9]{16}/),
7173
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
7274
data: {
73-
'sentry.origin': 'manual',
75+
'sentry.origin': 'auto.http.otel.fastify',
76+
'sentry.op': 'request-handler.fastify',
7477
'service.name': 'fastify',
7578
'hook.name': 'fastify -> @fastify/otel -> @fastify/middie - route-handler',
7679
'fastify.type': 'request-handler',
7780
'http.route': '/test-transaction',
7881
'hook.callback.name': 'anonymous',
7982
},
80-
description: 'handler - fastify -> @fastify/otel -> @fastify/middie',
83+
description: '@fastify/middie - route-handler',
84+
op: 'request-handler.fastify',
8185
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
8286
start_timestamp: expect.any(Number),
8387
timestamp: expect.any(Number),
8488
status: 'ok',
85-
origin: 'manual',
89+
origin: 'auto.http.otel.fastify',
8690
},
8791
{
8892
span_id: expect.stringMatching(/[a-f0-9]{16}/),

dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
6161
expect(spans).toContainEqual({
6262
data: {
6363
'plugin.name': 'sentry-fastify-error-handler',
64-
'fastify.type': 'middleware',
65-
'hook.name': 'onRequest',
64+
'fastify.type': 'request_handler',
65+
'http.route': '/test-transaction',
6666
'sentry.origin': 'auto.http.otel.fastify',
67-
'sentry.op': 'middleware.fastify',
67+
'sentry.op': 'request_handler.fastify',
6868
},
6969
description: 'sentry-fastify-error-handler',
70-
op: 'middleware.fastify',
70+
op: 'request_handler.fastify',
7171
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
7272
span_id: expect.stringMatching(/[a-f0-9]{16}/),
7373
start_timestamp: expect.any(Number),

dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ test('Sends an API route transaction', async ({ baseURL }) => {
6262
data: {
6363
'fastify.type': 'hook',
6464
'hook.callback.name': 'anonymous',
65-
'hook.name': 'fastify -> @fastify/otel -> sentry-fastify-error-handler - onRequest',
65+
'hook.name': 'fastify -> @fastify/otel - onRequest',
6666
'sentry.op': 'hook.fastify',
6767
'sentry.origin': 'auto.http.otel.fastify',
6868
'service.name': 'fastify',
6969
},
70-
description: 'sentry-fastify-error-handler - onRequest',
70+
description: '@fastify/otel - onRequest',
7171
op: 'hook.fastify',
7272
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
7373
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -78,7 +78,6 @@ test('Sends an API route transaction', async ({ baseURL }) => {
7878
origin: 'auto.http.otel.fastify',
7979
});
8080

81-
8281
expect(spans).toContainEqual({
8382
data: {
8483
'sentry.origin': 'manual',

dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ test('Sends an API route transaction', async ({ baseURL }) => {
6262
data: {
6363
'fastify.type': 'hook',
6464
'hook.callback.name': 'anonymous',
65-
'hook.name': 'fastify -> @fastify/otel -> sentry-fastify-error-handler - onRequest',
65+
'hook.name': 'fastify -> @fastify/otel - onRequest',
6666
'sentry.op': 'hook.fastify',
6767
'sentry.origin': 'auto.http.otel.fastify',
6868
'service.name': 'fastify',
6969
},
70-
description: 'sentry-fastify-error-handler - onRequest',
70+
description: '@fastify/otel - onRequest',
7171
op: 'hook.fastify',
7272
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
7373
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -78,7 +78,6 @@ test('Sends an API route transaction', async ({ baseURL }) => {
7878
origin: 'auto.http.otel.fastify',
7979
});
8080

81-
8281
expect(spans).toContainEqual({
8382
data: {
8483
'sentry.origin': 'manual',

packages/node/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"access": "public"
6666
},
6767
"dependencies": {
68-
"@fastify/otel": "0.5.2",
68+
"@fastify/otel": "fastify/otel#main",
6969
"@opentelemetry/api": "^1.9.0",
7070
"@opentelemetry/context-async-hooks": "^1.30.1",
7171
"@opentelemetry/core": "^1.30.1",
@@ -101,7 +101,8 @@
101101
"import-in-the-middle": "^1.13.0"
102102
},
103103
"devDependencies": {
104-
"@types/node": "^18.19.1"
104+
"@types/node": "^18.19.1",
105+
"fastify": "^5.3.0"
105106
},
106107
"scripts": {
107108
"build": "run-p build:transpile build:types",

packages/node/src/integrations/tracing/fastify/index.ts

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import FastifyOtelInstrumentation from '@fastify/otel';
2+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
23
import {
34
SEMANTIC_ATTRIBUTE_SENTRY_OP,
45
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
@@ -14,7 +15,6 @@ import { generateInstrumentOnce } from '../../../otel/instrument';
1415
import { FastifyInstrumentationV3 } from './v3/instrumentation';
1516
import * as diagnosticsChannel from 'node:diagnostics_channel';
1617
import { DEBUG_BUILD } from '../../../debug-build';
17-
import type { FastifyInstance, FastifyReply, FastifyRequest } from './types';
1818

1919
interface FastifyHandlerOptions {
2020
/**
@@ -54,28 +54,25 @@ interface FastifyHandlerOptions {
5454
const INTEGRATION_NAME = 'Fastify';
5555
const INTEGRATION_NAME_V3 = 'Fastify-V3';
5656

57-
export const instrumentFastifyV3 = generateInstrumentOnce(
58-
INTEGRATION_NAME_V3,
59-
() =>
60-
new FastifyInstrumentationV3({
61-
requestHook(span) {
62-
addFastifySpanAttributes(span);
63-
},
64-
}),
65-
);
57+
export const instrumentFastifyV3 = generateInstrumentOnce(INTEGRATION_NAME_V3, () => new FastifyInstrumentationV3());
6658

6759
export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME, () => {
68-
// FastifyOtelInstrumentation does not have a `requestHook`
69-
// so we can't use `addFastifySpanAttributes` here for now
7060
const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation();
7161
const plugin = fastifyOtelInstrumentationInstance.plugin();
7262

63+
// This message handler works for Fastify versions 3, 4 and 5
7364
diagnosticsChannel.subscribe('fastify.initialization', message => {
7465
const fastifyInstance = (message as { fastify?: FastifyInstance }).fastify;
7566

7667
fastifyInstance?.register(plugin).after(err => {
7768
if (err) {
7869
DEBUG_BUILD && logger.error('Failed to setup Fastify instrumentation', err);
70+
} else {
71+
instrumentClient();
72+
73+
if (fastifyInstance) {
74+
instrumentOnRequest(fastifyInstance);
75+
}
7976
}
8077
});
8178
});
@@ -153,18 +150,6 @@ export function setupFastifyErrorHandler(fastify: FastifyInstance, options?: Par
153150
}
154151
});
155152

156-
// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
157-
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
158-
// https://fastify.dev/docs/latest/Reference/Lifecycle/
159-
fastify.addHook('onRequest', async (request, _reply) => {
160-
// Taken from Otel Fastify instrumentation:
161-
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
162-
const routeName = request.routeOptions?.url || request.routerPath;
163-
const method = request.method || 'GET';
164-
165-
getIsolationScope().setTransactionName(`${method} ${routeName}`);
166-
});
167-
168153
done();
169154
},
170155
{
@@ -173,43 +158,67 @@ export function setupFastifyErrorHandler(fastify: FastifyInstance, options?: Par
173158
},
174159
);
175160

176-
// Sadly, middleware spans do not go through `requestHook`, so we handle those here
177-
// We register this hook in this method, because if we register it in the integration `setup`,
178-
// it would always run even for users that are not even using fastify
179-
const client = getClient();
180-
if (client) {
181-
client.on('spanStart', span => {
182-
addFastifySpanAttributes(span);
183-
});
184-
}
185-
161+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
186162
fastify.register(plugin);
187163
}
188164

189165
function addFastifySpanAttributes(span: Span): void {
190-
const attributes = spanToJSON(span).data;
166+
const spanJSON = spanToJSON(span);
167+
const spanName = spanJSON.description;
168+
const attributes = spanJSON.data;
191169

192-
// this is one of: middleware, request_handler
193170
const type = attributes['fastify.type'];
194171

172+
const isHook = type === 'hook';
173+
const isHandler = type === spanName?.startsWith('handler -');
174+
// In @fastify/otel `request-handler` is separated by dash, not underscore
175+
const isRequestHandler = spanName === 'request' || type === 'request-handler';
176+
195177
// If this is already set, or we have no fastify span, no need to process again...
196-
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
178+
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || (!isHandler && !isRequestHandler && !isHook)) {
197179
return;
198180
}
199181

182+
const opPrefix = isHook ? 'hook' : isHandler ? 'middleware' : isRequestHandler ? 'request-handler' : '<unknown>';
183+
200184
span.setAttributes({
201185
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.fastify',
202-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.fastify`,
186+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${opPrefix}.fastify`,
203187
});
204188

205-
// Also update the name, we don't need to "middleware - " prefix
206-
const name = attributes['fastify.name'] || attributes['plugin.name'] || attributes['hook.name'];
207-
if (typeof name === 'string') {
189+
const attrName = attributes['fastify.name'] || attributes['plugin.name'] || attributes['hook.name'];
190+
if (typeof attrName === 'string') {
208191
// Try removing `fastify -> ` and `@fastify/otel -> ` prefixes
209192
// This is a bit of a hack, and not always working for all spans
210193
// But it's the best we can do without a proper API
211-
const updatedName = name.replace(/^fastify -> /, '').replace(/^@fastify\/otel -> /, '');
194+
const updatedName = attrName.replace(/^fastify -> /, '').replace(/^@fastify\/otel -> /, '');
212195

213196
span.updateName(updatedName);
214197
}
215198
}
199+
200+
function instrumentClient(): void {
201+
const client = getClient();
202+
if (client) {
203+
client.on('spanStart', (span: Span) => {
204+
addFastifySpanAttributes(span);
205+
});
206+
}
207+
}
208+
209+
function instrumentOnRequest(fastify: FastifyInstance): void {
210+
fastify.addHook('onRequest', async (request: FastifyRequest, _reply) => {
211+
if (request.opentelemetry) {
212+
const { span } = request.opentelemetry();
213+
214+
if (span) {
215+
addFastifySpanAttributes(span);
216+
}
217+
}
218+
219+
const routeName = request.routeOptions?.url;
220+
const method = request.method || 'GET';
221+
222+
getIsolationScope().setTransactionName(`${method} ${routeName}`);
223+
});
224+
}

packages/node/src/integrations/tracing/fastify/v3/instrumentation.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ import {
2828
safeExecuteInTheMiddle,
2929
} from '@opentelemetry/instrumentation';
3030
import { SEMATTRS_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
31+
import type { Span } from '@sentry/core';
32+
import {
33+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
34+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
35+
getClient,
36+
getIsolationScope,
37+
spanToJSON,
38+
} from '@sentry/core';
3139

3240
import { AttributeNames, FastifyNames, FastifyTypes } from './enums/AttributeNames';
3341

@@ -101,6 +109,10 @@ export class FastifyInstrumentationV3 extends InstrumentationBase<FastifyInstrum
101109
if (routeName && rpcMetadata?.type === RPCType.HTTP) {
102110
rpcMetadata.route = routeName;
103111
}
112+
113+
const method = request.method || 'GET';
114+
115+
getIsolationScope().setTransactionName(`${method} ${routeName}`);
104116
done();
105117
};
106118
}
@@ -197,6 +209,8 @@ export class FastifyInstrumentationV3 extends InstrumentationBase<FastifyInstrum
197209
app.addHook('onRequest', instrumentation._hookOnRequest());
198210
app.addHook('preHandler', instrumentation._hookPreHandler());
199211

212+
instrumentClient();
213+
200214
instrumentation._wrap(app, 'addHook', instrumentation._wrapAddHook());
201215

202216
return app;
@@ -249,7 +263,6 @@ export class FastifyInstrumentationV3 extends InstrumentationBase<FastifyInstrum
249263
const anyRequest = request as any;
250264

251265
const handler = anyRequest.routeOptions?.handler || anyRequest.context?.handler;
252-
253266
const handlerName = handler?.name.startsWith('bound ') ? handler.name.substring(6) : handler?.name;
254267
const spanName = `${FastifyNames.REQUEST_HANDLER} - ${handlerName || this.pluginName || ANONYMOUS_NAME}`;
255268

@@ -266,6 +279,8 @@ export class FastifyInstrumentationV3 extends InstrumentationBase<FastifyInstrum
266279
}
267280
const span = startSpan(reply, instrumentation.tracer, spanName, spanAttributes);
268281

282+
addFastifyV3SpanAttributes(span);
283+
269284
const { requestHook } = instrumentation.getConfig();
270285
if (requestHook) {
271286
safeExecuteInTheMiddle(
@@ -285,3 +300,40 @@ export class FastifyInstrumentationV3 extends InstrumentationBase<FastifyInstrum
285300
};
286301
}
287302
}
303+
304+
function instrumentClient(): void {
305+
const client = getClient();
306+
if (client) {
307+
client.on('spanStart', (span: Span) => {
308+
addFastifyV3SpanAttributes(span);
309+
});
310+
}
311+
}
312+
313+
function addFastifyV3SpanAttributes(span: Span): void {
314+
const attributes = spanToJSON(span).data;
315+
316+
// this is one of: middleware, request_handler
317+
const type = attributes['fastify.type'];
318+
319+
// If this is already set, or we have no fastify span, no need to process again...
320+
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
321+
return;
322+
}
323+
324+
span.setAttributes({
325+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.fastify',
326+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.fastify`,
327+
});
328+
329+
// Also update the name, we don't need to "middleware - " prefix
330+
const name = attributes['fastify.name'] || attributes['plugin.name'] || attributes['hook.name'];
331+
if (typeof name === 'string') {
332+
// Try removing `fastify -> ` and `@fastify/otel -> ` prefixes
333+
// This is a bit of a hack, and not always working for all spans
334+
// But it's the best we can do without a proper API
335+
const updatedName = name.replace(/^fastify -> /, '').replace(/^@fastify\/otel -> /, '');
336+
337+
span.updateName(updatedName);
338+
}
339+
}

0 commit comments

Comments
 (0)