From 44604b9800615beb27e7b9ec20f50b1ec0ee68ec Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Mon, 3 Aug 2026 05:51:51 +0000 Subject: [PATCH 1/4] Add reproduction for opentelemetry/OtelTracer issue --- .../test/OtelTracerActiveParentRepro.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/opentelemetry/test/OtelTracerActiveParentRepro.test.ts diff --git a/packages/opentelemetry/test/OtelTracerActiveParentRepro.test.ts b/packages/opentelemetry/test/OtelTracerActiveParentRepro.test.ts new file mode 100644 index 00000000000..d061b0960ef --- /dev/null +++ b/packages/opentelemetry/test/OtelTracerActiveParentRepro.test.ts @@ -0,0 +1,50 @@ +import * as OtelTracer from "@effect/opentelemetry/OtelTracer" +import { assert, it } from "@effect/vitest" +import * as OtelApi from "@opentelemetry/api" +import * as EffectContext from "effect/Context" +import * as Option from "effect/Option" + +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 + + 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.deepStrictEqual( + [receivedParent?.traceState?.serialize(), receivedParent?.isRemote], + ["vendor=value", false] + ) +}) From 70ef0409c91cb7b6a22ad8e17ee853f1e6d99905 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 4 Aug 2026 09:30:26 +1200 Subject: [PATCH 2/4] Preserve active OpenTelemetry parent context --- .changeset/preserve-otel-parent-context.md | 5 ++ packages/opentelemetry/src/OtelTracer.ts | 22 ++++++-- .../opentelemetry/test/OtelTracer.test.ts | 48 ++++++++++++++++++ .../test/OtelTracerActiveParentRepro.test.ts | 50 ------------------- 4 files changed, 72 insertions(+), 53 deletions(-) create mode 100644 .changeset/preserve-otel-parent-context.md delete mode 100644 packages/opentelemetry/test/OtelTracerActiveParentRepro.test.ts 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..f6995fa636d 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 (Predicate.isNotUndefined(otelParent)) { + if (Predicate.isUndefined(annotations)) return otelParent + const traceFlags = extractTraceService(span, annotations, OtelTraceFlags) + const traceState = extractTraceService(span, annotations, OtelTraceState) + return { + ...otelParent, + traceFlags: traceFlags ?? otelParent.traceFlags, + ...(traceState ? { 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() diff --git a/packages/opentelemetry/test/OtelTracerActiveParentRepro.test.ts b/packages/opentelemetry/test/OtelTracerActiveParentRepro.test.ts deleted file mode 100644 index d061b0960ef..00000000000 --- a/packages/opentelemetry/test/OtelTracerActiveParentRepro.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as OtelTracer from "@effect/opentelemetry/OtelTracer" -import { assert, it } from "@effect/vitest" -import * as OtelApi from "@opentelemetry/api" -import * as EffectContext from "effect/Context" -import * as Option from "effect/Option" - -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 - - 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.deepStrictEqual( - [receivedParent?.traceState?.serialize(), receivedParent?.isRemote], - ["vendor=value", false] - ) -}) From 4a6dbee9dd298051b9a1932d64310cf96b20fe40 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 4 Aug 2026 09:39:54 +1200 Subject: [PATCH 3/4] Use direct undefined checks for OTel parent context --- packages/opentelemetry/src/OtelTracer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opentelemetry/src/OtelTracer.ts b/packages/opentelemetry/src/OtelTracer.ts index f6995fa636d..17ae1c84455 100644 --- a/packages/opentelemetry/src/OtelTracer.ts +++ b/packages/opentelemetry/src/OtelTracer.ts @@ -543,8 +543,8 @@ const makeSpanContext = ( annotations?: Context.Context ): Otel.SpanContext => { const otelParent = Context.getOrUndefined(span.annotations, OtelParentSpanContext) - if (Predicate.isNotUndefined(otelParent)) { - if (Predicate.isUndefined(annotations)) return otelParent + if (otelParent !== undefined) { + if (annotations === undefined) return otelParent const traceFlags = extractTraceService(span, annotations, OtelTraceFlags) const traceState = extractTraceService(span, annotations, OtelTraceState) return { From 5dd44a35abb2b73621b695c05efb793328ff565c Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 4 Aug 2026 09:52:32 +1200 Subject: [PATCH 4/4] Simplify OTel parent trace state handling --- packages/opentelemetry/src/OtelTracer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opentelemetry/src/OtelTracer.ts b/packages/opentelemetry/src/OtelTracer.ts index 17ae1c84455..b161db8f176 100644 --- a/packages/opentelemetry/src/OtelTracer.ts +++ b/packages/opentelemetry/src/OtelTracer.ts @@ -550,7 +550,7 @@ const makeSpanContext = ( return { ...otelParent, traceFlags: traceFlags ?? otelParent.traceFlags, - ...(traceState ? { traceState } : {}) + traceState: traceState ?? otelParent.traceState! } } const traceFlags = makeTraceFlags(span, annotations)