Skip to content

@effect/opentelemetry: Effect.withSpan ignores the active OpenTelemetry span when there is no Effect parent #8489

Description

@guillempuche

What happens

When plain JavaScript code is already inside an OpenTelemetry span, and it runs an Effect program that calls Effect.withSpan, the Effect span starts a new trace. It should become a child of the OpenTelemetry span that is already active.

The OtelTracer docs say parentage "is taken from Effect spans first and can also attach to the active OpenTelemetry context". That fallback exists, but it never runs from normal code.

This matters when Effect runs inside code instrumented by something else, for example a library that creates its own OpenTelemetry spans and then calls into Effect.

Reproduction

effect@4.0.0-rc.117, @effect/opentelemetry@4.0.0-rc.117, @opentelemetry/* 2.11.0, Node 24.

import { context, trace } from "@opentelemetry/api"
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks"
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"
import { Effect, Layer } from "effect"
import * as OtelTracer from "@effect/opentelemetry/OtelTracer"
import * as Resource from "@effect/opentelemetry/Resource"

const exporter = new InMemorySpanExporter()
context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable())
trace.setGlobalTracerProvider(new NodeTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] }))
const Live = Layer.provide(OtelTracer.layerGlobal, Resource.layer({ serviceName: "t" }))

const outer = trace.getTracer("plain").startSpan("otel-outer")
await context.with(trace.setSpan(context.active(), outer), () =>
  Effect.runPromise(Effect.void.pipe(Effect.withSpan("child"), Effect.provide(Live))))
outer.end()

const child = exporter.getFinishedSpans().find((s) => s.name === "child")
console.log("same trace:", child.spanContext().traceId === outer.spanContext().traceId)
console.log("parent:", child.parentSpanContext?.spanId, "expected:", outer.spanContext().spanId)

Output:

same trace: false
parent: undefined expected: 5f23378c868c8821

Expected: same trace: true, and the parent is otel-outer.

Why

  • packages/effect/src/internal/effect.ts (line 5961 at rc.117) creates spans with root: options?.root ?? Option.isNone(parent). With no Effect parent span, root is always true.
  • packages/opentelemetry/src/OtelTracer.ts (lines 421-424) only looks at the active OpenTelemetry context when root !== true, so it never does.
  • The existing test (packages/opentelemetry/test/OtelTracer.test.ts, around line 165) reaches the fallback only by building OtelSpan directly with root: false, not through Effect.withSpan.

In Effect 3 (checked on 3.19.0) this worked by default, because core did not set root itself. So it looks like a regression in v4.

Workarounds that work today

  • Effect.withSpan("child", { root: false }) on the outermost Effect span. Same setup as above, and the span then has the right parent.
  • OtelTracer.withSpanContext(outer.spanContext()), or Effect.withParentSpan(OtelTracer.makeExternalSpan(outer.spanContext())). Both need the OpenTelemetry span passed in by hand.

Possible fix

Only set root: true when the caller asks for it (for example root: options?.root === true), and add a test that goes through Effect.withSpan inside context.with(...).

Related: #6343 (a similar symptom with annotateCurrentSpan), and #6902, which fixed other parts of this same fallback.

Context

Found while building mastra-effect, an Effect v4 HTTP server adapter for Mastra. Mastra's code runs inside plain JavaScript under an Effect request, which is exactly this situation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions