Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/preserve-otel-parent-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/opentelemetry": patch
---

Preserve trace state and locality when adapting active OpenTelemetry parent contexts.
22 changes: 19 additions & 3 deletions packages/opentelemetry/src/OtelTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,25 +518,41 @@ 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<never>
): Option.Option<Tracer.AnySpan> => {
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)
}))
}

const makeSpanContext = (
span: Tracer.AnySpan,
annotations?: Context.Context<never>
): 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 ({
Expand Down
48 changes: 48 additions & 0 deletions packages/opentelemetry/test/OtelTracer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => ({
Expand Down Expand Up @@ -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()
Expand Down
Loading