Skip to content

Commit 453a357

Browse files
authored
fix!: move span out of H3EventContext (#13)
1 parent 5a50c5a commit 453a357

File tree

10 files changed

+135
-45
lines changed

10 files changed

+135
-45
lines changed

src/augment.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import type { Span } from "@opentelemetry/api";
1+
import type { Context, Span } from "@opentelemetry/api";
22
import type { H3Event } from "h3"
33
import { Presets } from "./types";
44

55
declare module 'h3' {
6-
interface H3EventContext {
7-
span: Span
6+
interface H3Event {
7+
otel: {
8+
span: Span
9+
/**
10+
* @internal
11+
*/
12+
__endTime: number|undefined
13+
ctx: Context
14+
}
815
}
916
}
1017

src/runtime/plugin.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@ import { ATTR_URL_PATH, ATTR_URL_FULL, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPO
44
import type { NitroAppPlugin, NitroRuntimeHooks } from "nitropack";
55
import { getResponseStatus, getRequestProtocol, getRequestURL, getHeaders } from "h3"
66

7-
const context = api.context, trace = api.trace
8-
7+
const context = api.context, trace = api.trace
98
export default <NitroAppPlugin>((nitro) => {
10-
119
nitro.hooks.hook('request', async (event) => {
1210
const tracer = trace.getTracer('nitro-opentelemetry')
1311
const requestURL = getRequestURL(event)
14-
const currentContext = context.active()
12+
const currentContext = context.active()
1513

1614
// Extract the parent context from the headers if it exists
17-
const parentCtx = api.propagation.extract(currentContext, getHeaders(event));
18-
19-
const span = tracer.startSpan(await getSpanName(event), {
15+
// If a span is already set in the context, use it as the parent
16+
const parentCtx = trace.getSpan(currentContext) ? currentContext : api.propagation.extract(currentContext, getHeaders(event));
17+
const span = tracer.startSpan(await getSpanName(event), {
2018
attributes: {
2119
[ATTR_URL_PATH]: event.context.matchedRoute?.path || event.path,
2220
[ATTR_URL_FULL]: event.path,
@@ -27,27 +25,30 @@ export default <NitroAppPlugin>((nitro) => {
2725
},
2826
kind: api.SpanKind.SERVER
2927
}, parentCtx)
30-
trace.setSpan(context.active(), span)
31-
event.context.span = span
32-
event.context.__otel = {}
28+
const ctx = trace.setSpan(context.active(), span)
29+
event.otel = {
30+
span,
31+
__endTime: undefined
32+
,ctx
33+
}
3334
})
3435

3536
nitro.hooks.hook('beforeResponse', (event) => {
36-
event.context.__otel.endTime = Date.now()
37+
event.otel.__endTime = Date.now()
3738
})
3839

3940
nitro.hooks.hook('afterResponse', async (event) => {
40-
event.context.span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, getResponseStatus(event))
41-
await nitro.hooks.callHook('otel:span:end', { event, span: event.context.span })
42-
event.context.span.end(event.context.__otel.endTime)
41+
event.otel.span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, getResponseStatus(event))
42+
await nitro.hooks.callHook('otel:span:end', { event, span: event.otel.span })
43+
event.otel.span.end(event.otel.__endTime)
4344
})
4445

4546
nitro.hooks.hook('error', async (error, { event }) => {
4647
if (event) {
47-
event.context.span.recordException(error)
48-
event.context.span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, getResponseStatus(event))
49-
await nitro.hooks.callHook('otel:span:end', { event, span: event.context.span })
50-
event.context.span.end()
48+
event.otel.span.recordException(error)
49+
event.otel.span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, getResponseStatus(event))
50+
await nitro.hooks.callHook('otel:span:end', { event, span: event.otel.span })
51+
event.otel.span.end()
5152
} else {
5253
const span = trace.getSpan(api.ROOT_CONTEXT)
5354
span?.recordException(error)

src/runtime/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as api from "@opentelemetry/api"
22
import { defineEventHandler } from "h3";
33

4-
const context = api.context, trace = api.trace
4+
const context = api.context
55

66
export function defineTracedEventHandler(handler: ReturnType<typeof defineEventHandler>) {
77
return defineEventHandler((event) => {
8-
return context.with(trace.setSpan(context.active(), event.context.span), handler, undefined, event)
8+
return context.with(event.otel.ctx, handler, undefined, event)
99
})
1010
}

test/fixtures/basic/init.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,21 @@ import * as opentelemetry from '@opentelemetry/sdk-node';
33
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
44
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
55
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
6-
6+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
7+
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
8+
9+
const contextManager = new AsyncLocalStorageContextManager();
10+
// Create and configure NodeTracerProvider
11+
const provider = new NodeTracerProvider();
12+
13+
// Initialize the provider
14+
provider.register({
15+
contextManager,
16+
});
17+
18+
719
const sdk = new opentelemetry.NodeSDK({
8-
traceExporter: new OTLPTraceExporter(),
20+
traceExporter: new OTLPTraceExporter(),
921
instrumentations: [getNodeAutoInstrumentations(), new UndiciInstrumentation()],
1022
});
1123
sdk.start();
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
export default defineTracedEventHandler((e) => {
22
// @ts-expect-error internal API
3-
const parentSpanId = e.context.span.parentSpanId
3+
const parentSpanId = e.otel.span.parentSpanId
44
return {
5-
traceId: e.context.span.spanContext().traceId,parentSpanId,
5+
traceId: e.otel.span.spanContext().traceId,
6+
parentSpanId,
67
}
78
})
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

22
export default defineEventHandler((event) => {
33
// @ts-expect-error - internal property
4-
return event.context.span.name
4+
return event.otel.span.name
55
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default defineTracedEventHandler(async (e) => {
2+
const { traceId, parentSpanId } = await e.$fetch('/another-endpoint')
3+
return {
4+
traceId: e.otel.span.spanContext().traceId,
5+
spanId: e.otel.span.spanContext().spanId,
6+
anotherEndpoint: {
7+
traceId,
8+
parentSpanId
9+
},
10+
// @ts-expect-error internal API
11+
parentSpanId: e.otel.span.parentSpanId
12+
}
13+
})

test/fixtures/basic/routes/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
export default defineTracedEventHandler(async (e) => {
2-
const { traceId, parentSpanId } = await $fetch('/another-endpoint')
2+
const { traceId, parentSpanId } = await globalThis.$fetch('/another-endpoint')
33
return {
4-
traceId: e.context.span.spanContext().traceId,
5-
spanId: e.context.span.spanContext().spanId,
4+
traceId: e.otel.span.spanContext().traceId,
5+
spanId: e.otel.span.spanContext().spanId,
66
anotherEndpoint: {
77
traceId,
88
parentSpanId

test/fixtures/basic/routes/wait-ms.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
export default defineTracedEventHandler(async (e) => {
3+
const ms = Number(getQuery(e).ms)
4+
5+
await new Promise((resolve) => setTimeout(resolve, ms))
6+
const { traceId, parentSpanId } = await globalThis.$fetch('/another-endpoint')
7+
return {
8+
traceId: e.otel.span.spanContext().traceId,
9+
spanId: e.otel.span.spanContext().spanId,
10+
anotherEndpoint: {
11+
traceId,
12+
parentSpanId
13+
}
14+
}
15+
})

test/index.test.ts

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,72 @@
11
import { describe, expect, it } from 'vitest'
22
import { $fetchRaw } from 'nitro-test-utils/e2e'
3-
const dummyTrace = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'
3+
const dummyTrace = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'
4+
const dummyTrace2 = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'
45

56

67
describe('traces', async () => {
78
it('should trace requests', async () => {
8-
const { data } = await $fetchRaw('')
9+
const { data } = await $fetchRaw('/')
910

1011
expect(data.traceId).toBeDefined()
1112
})
1213

1314
it('should keep trace context', async () => {
14-
15-
const { data } = await $fetchRaw('', {
15+
const { data } = await $fetchRaw('/', {
16+
headers: {
17+
traceparent: dummyTrace
18+
}
19+
})
20+
// assert that the traceId is the same
21+
expect(data.traceId).toBe(dummyTrace.split('-')[1])
22+
23+
// assert that localFetch is correctly traced
24+
expect(data.anotherEndpoint.traceId).toBe(data.traceId)
25+
expect(data.anotherEndpoint.parentSpanId).toBe(data.spanId)
26+
27+
})
28+
29+
it('event.$fetch', async () => {
30+
const { data } = await $fetchRaw('event-fetch', {
31+
headers: {
32+
traceparent: dummyTrace
33+
}
34+
})
35+
// assert that the traceId is the same
36+
expect(data.traceId).toBe(dummyTrace.split('-')[1])
37+
38+
// assert that localFetch is correctly traced
39+
expect(data.anotherEndpoint.traceId).toBe(data.traceId)
40+
expect(data.anotherEndpoint.parentSpanId).toBe(data.spanId)
41+
})
42+
43+
it('expect no XRSP', async () => {
44+
const [{data}, {data: data2}] = await Promise.all([
45+
$fetchRaw('/wait-ms?ms=350', {
1646
headers: {
17-
traceparent:dummyTrace
47+
traceparent: dummyTrace
1848
}
19-
})
20-
// assert that the traceId is the same
21-
expect(data.traceId).toBe(dummyTrace.split('-')[1])
22-
23-
// assert that localFetch is correctly traced
24-
expect(data.anotherEndpoint.traceId).toBe(data.traceId)
25-
expect(data.anotherEndpoint.parentSpanId).toBe(data.spanId)
26-
27-
})
49+
}),
50+
$fetchRaw('/wait-ms?ms=100', {
51+
headers: {
52+
traceparent: dummyTrace2
53+
}
54+
})
55+
])
2856

57+
58+
// assert that the traceId is the same
59+
expect(data.traceId).toBe(dummyTrace.split('-')[1])
60+
61+
// assert that localFetch is correctly traced
62+
expect(data.anotherEndpoint.traceId).toBe(data.traceId)
63+
expect(data.anotherEndpoint.parentSpanId).toBe(data.spanId)
64+
65+
// assert that localFetch is correctly traced
66+
expect(data2.traceId).toBe(dummyTrace2.split('-')[1])
67+
expect(data2.anotherEndpoint.traceId).toBe(data2.traceId)
68+
expect(data2.anotherEndpoint.parentSpanId).toBe(data2.spanId)
69+
})
2970
})
3071

3172
describe('names', () => {

0 commit comments

Comments
 (0)