Skip to content
Open
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
34 changes: 33 additions & 1 deletion packages/otel/src/bootstrap/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type SpanProcessor } from '@opentelemetry/sdk-trace-node'
import type { Instrumentation } from '@opentelemetry/instrumentation'
import { GET_TRACER, SHUTDOWN_TRACERS } from '../constants.js'
import { GET_TRACE_CONTEXT_FORWARDER, GET_TRACER, SHUTDOWN_TRACERS } from '../constants.js'
import { Context, context, W3CTraceContextPropagator } from '../opentelemetry.ts'

export interface TracerProviderOptions {
serviceName: string
Expand All @@ -11,6 +12,7 @@ export interface TracerProviderOptions {
siteName: string
instrumentations?: (Instrumentation | Promise<Instrumentation>)[]
spanProcessors?: (SpanProcessor | Promise<SpanProcessor>)[]
propagationHeaders?: Headers
}

export const createTracerProvider = async (options: TracerProviderOptions) => {
Expand Down Expand Up @@ -46,6 +48,27 @@ export const createTracerProvider = async (options: TracerProviderOptions) => {
propagator: new W3CTraceContextPropagator(),
})

let traceContextForwarder: (propagator: W3CTraceContextPropagator, requestHeaders: Headers) => Context

if (options.propagationHeaders) {
traceContextForwarder = (propagator: W3CTraceContextPropagator, requestHeaders: Headers): Context => {
const getter = {
keys: (carrier: Headers) => [...carrier.keys()],
get: (carrier: Headers, key: string) => carrier.get(key) ?? undefined,
}
const extractedContext = propagator.extract(context.active(), options.propagationHeaders, getter)

propagator.inject(context.active(), requestHeaders, {
set: (carrier, key, value) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
carrier.set(key, value)
},
})

return extractedContext
}
}

const instrumentations = await Promise.all(options.instrumentations ?? [])

registerInstrumentations({
Expand All @@ -70,6 +93,15 @@ export const createTracerProvider = async (options: TracerProviderOptions) => {
},
})

Object.defineProperty(globalThis, GET_TRACE_CONTEXT_FORWARDER, {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not totally convinced this is the best way to make this forwarder accessible to the fetch instrumentation so may rework this

enumerable: false,
configurable: true,
writable: false,
value: function () {
return traceContextForwarder
},
})

Object.defineProperty(globalThis, SHUTDOWN_TRACERS, {
enumerable: false,
configurable: true,
Expand Down
1 change: 1 addition & 0 deletions packages/otel/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const GET_TRACER = '__netlify__getTracer'
export const SHUTDOWN_TRACERS = '__netlify__shutdownTracers'
export const GET_TRACE_CONTEXT_FORWARDER = '__netlify__getTraceContextForwarder'
export const TRACE_PREFIX = '__nfOTLPTrace'
33 changes: 32 additions & 1 deletion packages/otel/src/instrumentations/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,15 @@ describe('header exclusion', () => {
})
})

let requestHeaders = new Headers()

describe('patched fetch', () => {
const server = setupServer(
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
http.get('http://localhost:3000/ok', () => HttpResponse.json({ message: 'ok' })),
http.get('http://localhost:3000/ok', ({ request }) => {
requestHeaders = request.headers
return HttpResponse.json({ message: 'ok' })
}),
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
http.post('http://localhost:3000/ok', () => HttpResponse.json({ message: 'ok' })),
)
Expand All @@ -98,6 +103,7 @@ describe('patched fetch', () => {
})

beforeEach(async () => {
requestHeaders = new Headers()
await createTracerProvider({
headers: new Headers({ 'x-nf-enable-tracing': 'true' }),
serviceName: 'test-service',
Expand Down Expand Up @@ -162,4 +168,29 @@ describe('patched fetch', () => {
})
await expect(fetch(req).then((r) => r.json())).resolves.toEqual({ message: 'ok' })
})

it('uses propagation headers to forward trace context', async () => {
const traceParent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'
await createTracerProvider({
propagationHeaders: new Headers({ traceparent: traceParent }),
serviceName: 'test-service',
serviceVersion: '1.0.0',
deploymentEnvironment: 'test',
siteUrl: 'https://example.com',
siteId: '12345',
siteName: 'example',
instrumentations: [new FetchInstrumentation()],
})

await expect(
fetch('http://localhost:3000/ok', { headers: new Headers({ 'some-header': 'value' }) }).then((r) => r.json()),
).resolves.toEqual({ message: 'ok' })

const forwardedTraceParent = requestHeaders.get('traceparent')
expect(forwardedTraceParent).toMatch(/^00-4bf92f3577b34da6a3ce929d0e0e4736-[0-9a-f]{16}-01$/)
expect(forwardedTraceParent).not.toBe(traceParent)

// ensure we do not strip existing headers
expect(requestHeaders.get('some-header')).toBe('value')
})
})
20 changes: 19 additions & 1 deletion packages/otel/src/instrumentations/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as api from '@opentelemetry/api'
import { SugaredTracer } from '@opentelemetry/api/experimental'
import { _globalThis } from '@opentelemetry/core'
import { _globalThis, W3CTraceContextPropagator } from '@opentelemetry/core'
import { InstrumentationConfig, type Instrumentation } from '@opentelemetry/instrumentation'
import { getTraceContextForwarder } from '../main.ts'

export interface FetchInstrumentationConfig extends InstrumentationConfig {
getRequestAttributes?(headers: Request): api.Attributes
Expand Down Expand Up @@ -120,6 +121,23 @@ export class FetchInstrumentation implements Instrumentation {
return await originalFetch(resource, options)
}

const traceContextForwarder = getTraceContextForwarder()
if (traceContextForwarder) {
const headers = options?.headers ? new Headers(options.headers) : new Headers()
const extractedContext = traceContextForwarder(new W3CTraceContextPropagator(), headers)

// Replace headers in options with the mutated version
const nextOptions: RequestInit = { ...options, headers }

return tracer.startActiveSpan('fetch', {}, extractedContext, async (span) => {
const request = new Request(resource, nextOptions)
this.annotateFromRequest(span, request)
const response = await originalFetch(request, nextOptions)
this.annotateFromResponse(span, response)
return response
})
}

return tracer.withActiveSpan('fetch', async (span) => {
const request = new Request(resource, options)
this.annotateFromRequest(span, request)
Expand Down
12 changes: 11 additions & 1 deletion packages/otel/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { type SugaredSpanOptions, type SugaredTracer } from '@opentelemetry/api/experimental'
import { GET_TRACER, SHUTDOWN_TRACERS } from './constants.js'
import { GET_TRACER, SHUTDOWN_TRACERS, GET_TRACE_CONTEXT_FORWARDER } from './constants.js'
import type { Context, Span } from '@opentelemetry/api'
import { W3CTraceContextPropagator } from '@opentelemetry/core'

type GlobalThisExtended = typeof globalThis & {
[GET_TRACER]?: (name?: string, version?: string) => SugaredTracer | undefined
[SHUTDOWN_TRACERS]?: () => void
[GET_TRACE_CONTEXT_FORWARDER]?: () =>
| ((propagator: W3CTraceContextPropagator, requestHeaders: Headers) => Context)
| undefined
}

export const getTracer = (name?: string, version?: string): SugaredTracer | undefined => {
return (globalThis as GlobalThisExtended)[GET_TRACER]?.(name, version)
}

export const getTraceContextForwarder = ():
| ((propagator: W3CTraceContextPropagator, requestHeaders: Headers) => Context)
| undefined => {
return (globalThis as GlobalThisExtended)[GET_TRACE_CONTEXT_FORWARDER]?.()
}

export const shutdownTracers = async (): Promise<void> => {
return (globalThis as GlobalThisExtended)[SHUTDOWN_TRACERS]?.()
}
Expand Down
Loading