diff --git a/.changeset/preserve-otel-parent-context.md b/.changeset/preserve-otel-parent-context.md new file mode 100644 index 00000000000..d436bd97150 --- /dev/null +++ b/.changeset/preserve-otel-parent-context.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Preserve trace state and locality when adapting active OpenTelemetry parent contexts. diff --git a/packages/opentelemetry/src/OtelTracer.ts b/packages/opentelemetry/src/OtelTracer.ts index 8e34a0abb73..b161db8f176 100644 --- a/packages/opentelemetry/src/OtelTracer.ts +++ b/packages/opentelemetry/src/OtelTracer.ts @@ -518,18 +518,23 @@ export class OtelSpan implements Tracer.Span { const isSampled = (traceFlags: Otel.TraceFlags): boolean => (traceFlags & Otel.TraceFlags.SAMPLED) === Otel.TraceFlags.SAMPLED +class OtelParentSpanContext extends Context.Service< + OtelParentSpanContext, + Otel.SpanContext +>()("@effect/opentelemetry/Tracer/OtelParentSpanContext") {} + const getOtelParent = ( tracer: Otel.TraceAPI, context: Otel.Context, annotations: Context.Context ): Option.Option => { - const otelParent = tracer.getSpan(context)?.spanContext() + const otelParent = tracer.getSpanContext(context) if (!otelParent) return Option.none() return Option.some(Tracer.externalSpan({ spanId: otelParent.spanId, traceId: otelParent.traceId, - sampled: (otelParent.traceFlags & 1) === 1, - annotations + sampled: isSampled(otelParent.traceFlags), + annotations: Context.add(annotations, OtelParentSpanContext, otelParent) })) } @@ -537,6 +542,17 @@ const makeSpanContext = ( span: Tracer.AnySpan, annotations?: Context.Context ): Otel.SpanContext => { + const otelParent = Context.getOrUndefined(span.annotations, OtelParentSpanContext) + if (otelParent !== undefined) { + if (annotations === undefined) return otelParent + const traceFlags = extractTraceService(span, annotations, OtelTraceFlags) + const traceState = extractTraceService(span, annotations, OtelTraceState) + return { + ...otelParent, + traceFlags: traceFlags ?? otelParent.traceFlags, + traceState: traceState ?? otelParent.traceState! + } + } const traceFlags = makeTraceFlags(span, annotations) const traceState = makeTraceState(span, annotations)! return ({ diff --git a/packages/opentelemetry/test/OtelTracer.test.ts b/packages/opentelemetry/test/OtelTracer.test.ts index d4f8df6f826..f6e010a27f9 100644 --- a/packages/opentelemetry/test/OtelTracer.test.ts +++ b/packages/opentelemetry/test/OtelTracer.test.ts @@ -5,7 +5,9 @@ import * as OtelApi from "@opentelemetry/api" import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks" import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" import * as Cause from "effect/Cause" +import * as EffectContext from "effect/Context" import * as Effect from "effect/Effect" +import * as Option from "effect/Option" import * as EffectTracer from "effect/Tracer" const TracingLive = NodeSdk.layer(Effect.sync(() => ({ @@ -102,6 +104,52 @@ describe("Tracer", () => { Effect.provide(TracingLive) )) + it("preserves trace state and locality on an active OpenTelemetry parent", () => { + const parent: OtelApi.SpanContext = { + traceId: "1".repeat(32), + spanId: "2".repeat(16), + traceFlags: OtelApi.TraceFlags.SAMPLED, + traceState: OtelApi.createTraceState("vendor=value"), + isRemote: false + } + const active = OtelApi.trace.setSpanContext(OtelApi.ROOT_CONTEXT, parent) + let receivedParent: OtelApi.SpanContext | undefined + const tracer = { + startSpan(_name: string, _options: unknown, context: OtelApi.Context) { + receivedParent = OtelApi.trace.getSpanContext(context) + return { + spanContext: () => ({ + traceId: "3".repeat(32), + spanId: "4".repeat(16), + traceFlags: OtelApi.TraceFlags.SAMPLED + }) + } as OtelApi.Span + } + } as OtelApi.Tracer + + const child = new OtelTracer.OtelSpan( + { active: () => active } as OtelApi.ContextAPI, + OtelApi.trace, + tracer, + { + name: "child", + parent: Option.none(), + annotations: EffectContext.empty(), + links: [], + startTime: 0n, + kind: "internal", + root: false, + sampled: true + } + ) + + assert.instanceOf(child, OtelTracer.OtelSpan) + assert.deepStrictEqual( + [receivedParent?.traceState?.serialize(), receivedParent?.isRemote], + ["vendor=value", false] + ) + }) + it.effect("records every pretty error", () => Effect.gen(function*() { const exporter = new InMemorySpanExporter()