Tracing
A trace records the path a request takes through your system as a tree of
spans, each measuring one unit of work. Effect builds this tree
automatically: when you wrap an effect with Effect.withSpan, any span created
inside that effect becomes a child, because parent/child relationships follow
the fiber’s context. You annotate spans with attributes, and at the edge of the
app you provide an exporter layer to ship the whole tree to a tracing backend.
import { Context, Effect, Layer } from "effect"
export class Checkout extends Context.Service<Checkout, { processCheckout(orderId: string): Effect.Effect<void>}>()("acme/Checkout") { static readonly layer = Layer.effect( Checkout, Effect.gen(function*() { return Checkout.of({ // Effect.fn names the function and opens a span around each call. processCheckout: Effect.fn("Checkout.processCheckout")(function*(orderId) { yield* Effect.logInfo("starting checkout", { orderId })
// A child span for the card charge, with attributes describing it. yield* Effect.sleep("50 millis").pipe( Effect.withSpan("checkout.charge-card"), Effect.annotateSpans({ "checkout.order_id": orderId, "checkout.provider": "acme-pay" }) )
// A sibling child span for persistence. yield* Effect.sleep("20 millis").pipe( Effect.withSpan("checkout.persist-order") )
yield* Effect.logInfo("checkout completed", { orderId }) }) }) }) )}Calling processCheckout produces a Checkout.processCheckout span with two
children, checkout.charge-card and checkout.persist-order. Each carries its
own duration, and the attributes you attach show up on the corresponding span in
your backend.
Creating spans
Section titled “Creating spans”Effect.withSpan(name, options) wraps an effect in a span that starts when the
effect begins and ends when it completes (including on failure or interruption).
The options mirror OpenTelemetry: attributes, kind
("server" | "client" | "internal" | "producer" | "consumer"), links,
root, and more.
import { Effect } from "effect"
const handler = Effect.gen(function*() { yield* Effect.sleep("10 millis")}).pipe( Effect.withSpan("handle-request", { kind: "server", attributes: { "http.method": "POST", "http.route": "/checkout" } }))Effect.fn("name")(function*…) is the idiomatic way to define an
effect-returning function: it both names the function for stack traces and opens
a span named after it for every call — so most of your spans come for free just
from writing functions this way.
Annotating the active span
Section titled “Annotating the active span”Use Effect.annotateSpans to attach attributes to every span created within an
effect, or Effect.annotateCurrentSpan to add an attribute to the innermost
span only. You can also reach the active span directly with Effect.currentSpan.
import { Effect } from "effect"
const work = Effect.gen(function*() { // Add an attribute to the currently active span. yield* Effect.annotateCurrentSpan("cache.hit", false)
// Access the span object itself when you need its trace/span ids. const span = yield* Effect.currentSpan yield* Effect.logInfo("inside span", { spanId: span.spanId, traceId: span.traceId })}).pipe(Effect.withSpan("work"))Scoped spans and layer spans
Section titled “Scoped spans and layer spans”Effect.withSpanScoped ties a span’s lifetime to a Scope instead of a single
effect — handy when a span should stay open across several steps of a resource’s
lifecycle. Layer.withSpan wraps the construction of a layer in a span, which
is useful for tracing application startup.
import { Effect, Layer } from "effect"
// Trace the work done while a layer is being built.const Setup = Layer.effectDiscard( Effect.gen(function*() { yield* Effect.logInfo("running migrations") yield* Effect.sleep("30 millis") }).pipe(Effect.withSpan("startup.migrate"))).pipe(Layer.withSpan("startup"))Exporting traces
Section titled “Exporting traces”By itself, Effect builds the span tree but does not send it anywhere. You enable
export by providing a tracer layer at the edge of the application. The
lightest-weight option is OtlpTracer from effect/unstable/observability,
which POSTs spans to any OpenTelemetry collector:
import { Layer } from "effect"import { FetchHttpClient } from "effect/unstable/http"import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"
const Tracing = OtlpTracer.layer({ url: "http://localhost:4318/v1/traces", resource: { serviceName: "checkout-api", serviceVersion: "1.0.0" }}).pipe( // OtlpTracer needs a serializer and an HttpClient to do the POSTing. Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))Provide Tracing last, after your application layers, so every span the app
creates is captured before being exported. For the full configuration surface
(batching, headers, resource attributes), OtlpLogger, PrometheusMetrics,
and the @effect/opentelemetry integration for existing OpenTelemetry SDKs, see
the Exporters page.
Effect span reference
Section titled “Effect span reference”Everything above is built on a handful of combinators on the Effect module.
This section enumerates them; the data types they accept (SpanOptions,
SpanKind, SpanLink, …) are documented in the Tracer module
reference below.
Effect.withSpan
Section titled “Effect.withSpan”Wraps an effect in a child span that opens when the effect starts and ends when
it completes — on success, failure, or interruption. The second argument is a
SpanOptions object (see the fields below); it can also be a function of the
function arguments when used as the trailing combinator of Effect.fn.
import { Effect } from "effect"
const traced = Effect.succeed(42).pipe( Effect.withSpan("compute", { attributes: { tier: "free" } }))// => span "compute" with attribute tier=free wraps the success of 42SpanOptions (= SpanOptionsNoTrace + TraceOptions) accepts the following
fields, all optional:
attributes?: Record<string, unknown>— key/value tags recorded on the span.links?: ReadonlyArray<SpanLink>— links to other (possibly external) spans.parent?: AnySpan— force a specific parent instead of the contextual one.root?: boolean— whentrue, start a brand-new trace (ignore any parent).annotations?: Context.Context<never>— context-level annotations to attach.kind?: SpanKind—"internal"(default) |"server"|"client"|"producer"|"consumer".sampled?: boolean— explicit sampling decision; bypasses trace-level gating.level?: LogLevel— the trace level used byMinimumTraceLevelgating.captureStackTrace?: boolean | (() => string | undefined)— capture (or supply) a stack trace for the span; setfalseto skip the cost.
import { Effect, Tracer } from "effect"
const upstream = Tracer.externalSpan({ spanId: "s1", traceId: "t1" })
const child = Effect.succeed("ok").pipe( Effect.withSpan("handle", { kind: "server", parent: upstream, links: [{ span: upstream, attributes: { relationship: "follows" } }], attributes: { "http.route": "/checkout" } }))// => span "handle" parented to the external trace t1, kind=serverEffect.withSpanScoped
Section titled “Effect.withSpanScoped”Like withSpan, but the span ends when the surrounding Scope is finalized
rather than when the effect completes. Use it when a span must stay open across
several steps of a resource’s lifecycle.
import { Effect } from "effect"
const program = Effect.scoped( Effect.gen(function*() { yield* Effect.withSpanScoped(Effect.logInfo("step 1"), "session") yield* Effect.logInfo("step 2") // still inside the "session" span }))// => "session" span covers both steps, closed when the scope endsEffect.makeSpan
Section titled “Effect.makeSpan”Creates a span value and returns it without installing it as the current
parent and without ending it automatically — you control its lifecycle by
hand (or hand it to withParentSpan).
import { Effect } from "effect"
const program = Effect.gen(function*() { const span = yield* Effect.makeSpan("manual-op") return span.name})// => "manual-op" (you are responsible for ending it)Effect.makeSpanScoped
Section titled “Effect.makeSpanScoped”Creates a standalone span and registers a finalizer that ends it when the
Scope closes. The span is not pushed onto the span stack, so no child spans
attach to it implicitly.
import { Effect } from "effect"
const program = Effect.scoped( Effect.gen(function*() { const span = yield* Effect.makeSpanScoped("resource-lifetime") yield* Effect.logInfo("using resource") return span.spanId // => span ends automatically here }))Effect.useSpan
Section titled “Effect.useSpan”Runs a callback with a freshly created span and ends it when the callback
completes. The span is standalone (not added to the stack), which makes it the
low-level building block behind withSpan.
import { Effect } from "effect"
const program = Effect.useSpan("user-op", (span) => Effect.gen(function*() { span.attribute("user.id", "123") return "done" }))// => "done", with span "user-op" carrying attribute user.id=123Effect.withParentSpan
Section titled “Effect.withParentSpan”Pushes a span (local or external) onto the current span stack so that spans
created inside the effect become its children. Pair it with makeSpan or
Tracer.externalSpan to graft work under a chosen parent.
import { Effect, Tracer } from "effect"
const parent = Tracer.externalSpan({ spanId: "s9", traceId: "t9" })
const program = Effect.succeed("child work").pipe( Effect.withSpan("child"), Effect.withParentSpan(parent))// => "child" span is parented to the external trace t9Effect.annotateSpans
Section titled “Effect.annotateSpans”Adds an attribute (or a record of attributes) to every span created inside
the effect. Dual: data-first (effect, key, value) or data-last for .pipe.
import { Effect } from "effect"
const program = Effect.succeed(1).pipe( Effect.withSpan("a"), Effect.annotateSpans({ tenant: "acme", region: "us-east-1" }))// => both attributes appear on span "a" (and any nested spans)Effect.annotateCurrentSpan
Section titled “Effect.annotateCurrentSpan”Adds an attribute (or record) to the innermost active span only, leaving
outer spans untouched. Returns Effect<void>.
import { Effect } from "effect"
const work = Effect.gen(function*() { yield* Effect.annotateCurrentSpan("cache.hit", true)}).pipe(Effect.withSpan("lookup"))// => attribute cache.hit=true on span "lookup" onlyEffect.currentSpan
Section titled “Effect.currentSpan”Yields the currently active local Span, or fails with NoSuchElementError
when there is no active span. Use it to read the span’s spanId / traceId,
e.g. to correlate with external systems.
import { Effect } from "effect"
const program = Effect.gen(function*() { const span = yield* Effect.currentSpan return span.traceId}).pipe(Effect.withSpan("op"))// => the active trace id (fails with NoSuchElementError if no span is active)Effect.currentParentSpan
Section titled “Effect.currentParentSpan”Yields the current parent span — which may be a local Span or an
ExternalSpan — or fails with NoSuchElementError when no parent is present.
import { Effect } from "effect"
const child = Effect.gen(function*() { const parent = yield* Effect.currentParentSpan return parent._tag // "Span" | "ExternalSpan"}).pipe(Effect.withSpan("child"), Effect.withSpan("parent"))// => "Span" (the enclosing "parent" span)Effect.spanAnnotations
Section titled “Effect.spanAnnotations”Returns the span annotations currently carried in the effect context as a plain record. These are applied to spans created within the context.
import { Effect } from "effect"
const program = Effect.gen(function*() { return yield* Effect.spanAnnotations}).pipe(Effect.annotateSpans({ userId: "123" }))// => { userId: "123" }Effect.spanLinks
Section titled “Effect.spanLinks”Returns the SpanLinks currently carried in the effect context. Span links
connect related spans without a parent/child relationship.
import { Effect } from "effect"
const program = Effect.gen(function*() { const links = yield* Effect.spanLinks return links.length})// => 0 (no links provided in this context)Effect.linkSpans
Section titled “Effect.linkSpans”Adds a link to the provided span (or array of spans), with optional attributes, on every span created inside the effect. Useful for fan-out/fan-in or cross-trace relationships.
import { Effect } from "effect"
const program = Effect.gen(function*() { const here = yield* Effect.currentSpan return yield* Effect.succeed("work").pipe( Effect.withSpan("follow-up"), Effect.linkSpans(here, { relationship: "follows" }) )}).pipe(Effect.withSpan("origin"))// => span "follow-up" carries a link to "origin"Tracer controls: withTracer, withTracerEnabled, withTracerTiming
Section titled “Tracer controls: withTracer, withTracerEnabled, withTracerTiming”Effect.withTracer swaps the active Tracer for a region of code.
Effect.withTracerEnabled(false) skips registering spans with the tracer (they
still form the local stack but are not exported). Effect.withTracerTiming(false)
drops timing information from spans.
import { Effect } from "effect"
const noisy = Effect.succeed(1).pipe( Effect.withSpan("health-check"), // Don't ship this span to the backend, and don't record timing. Effect.withTracerEnabled(false), Effect.withTracerTiming(false))// => span exists locally but is not registered/timedEffect.tracer
Section titled “Effect.tracer”An effect that yields the active Tracer from context — the backend responsible
for allocating spans. Most code never needs this; reach for it when building
tooling on top of the tracer.
import { Effect } from "effect"
const program = Effect.gen(function*() { const tracer = yield* Effect.tracer return typeof tracer.span})// => "function"Tracer module reference
Section titled “Tracer module reference”The Tracer module (import { Tracer } from "effect") holds the low-level
tracing data model: the backend interface, span types, and the context
references that control propagation and sampling. Application code rarely
touches it directly — Effect.withSpan and friends are the everyday surface —
but it is what custom backends and external-trace integrations are built on.
Tracer (interface) and Tracer.make
Section titled “Tracer (interface) and Tracer.make”A Tracer is a backend with a single span(options) method that allocates a
Span from a name, parent, annotations, links, start time, kind, root flag, and
sampling decision. Tracer.make is the identity constructor that brands an
implementation object as a Tracer.
import { Tracer } from "effect"
const custom = Tracer.make({ span: (options) => new Tracer.NativeSpan(options)})// => a Tracer that delegates to the built-in NativeSpanTracer.Tracer (context reference)
Section titled “Tracer.Tracer (context reference)”The active tracer service, keyed by Tracer.TracerKey ("effect/Tracer"). Its
default value is the native tracer, which produces NativeSpan instances.
Provide a different value (e.g. via Effect.withTracer or an exporter layer) to
change where spans go.
import { Effect, Tracer } from "effect"
const program = Effect.gen(function*() { const tracer = yield* Effect.service(Tracer.Tracer) return typeof tracer.span})// => "function"
console.log(Tracer.TracerKey)// => "effect/Tracer"Tracer.Span
Section titled “Tracer.Span”A span produced by an Effect tracer. It carries name, spanId, traceId,
parent, attributes, links, sampled, kind, and lifecycle status, plus
methods end, attribute, event, and addLinks.
import { Effect } from "effect"
const program = Effect.gen(function*() { const span = yield* Effect.currentSpan span.attribute("step", "validate") return span._tag}).pipe(Effect.withSpan("op"))// => "Span"Tracer.ExternalSpan and Tracer.AnySpan
Section titled “Tracer.ExternalSpan and Tracer.AnySpan”ExternalSpan represents a span imported from another tracing system: it
carries identity (spanId, traceId), sampled, and annotations, but has no
lifecycle methods. AnySpan is the union Span | ExternalSpan accepted wherever
a span can act as a parent or link.
import { Context } from "effect"import type { Tracer } from "effect"
const external: Tracer.ExternalSpan = { _tag: "ExternalSpan", spanId: "span-abc", traceId: "trace-xyz", sampled: true, annotations: Context.empty()}// => use as parent/link, but you cannot call .end() on itTracer.externalSpan
Section titled “Tracer.externalSpan”Constructs an ExternalSpan from spanId / traceId, defaulting sampled to
true and annotations to an empty context. This is how you adopt an upstream
trace context (e.g. from incoming HTTP headers) so Effect spans nest under it.
import { Effect, Tracer } from "effect"
const parent = Tracer.externalSpan({ spanId: "s1", traceId: "t1" })
const program = Effect.succeed("ok").pipe( Effect.withSpan("child", { parent }))// => "child" span belongs to the upstream trace t1Tracer.SpanStatus
Section titled “Tracer.SpanStatus”A span’s lifecycle state: { _tag: "Started", startTime } or
{ _tag: "Ended", startTime, endTime, exit }. The exit records how the span’s
work completed.
import { Exit } from "effect"import type { Tracer } from "effect"
const ended: Tracer.SpanStatus = { _tag: "Ended", startTime: 1_000_000_000n, endTime: 1_500_000_000n, exit: Exit.succeed("result")}// => ended.endTime - ended.startTime === 500_000_000nTracer.SpanKind
Section titled “Tracer.SpanKind”The OpenTelemetry role of a span: "internal" | "server" | "client" | "producer" | "consumer". Passed via the kind option of withSpan.
import { Effect } from "effect"import type { Tracer } from "effect"
const kind: Tracer.SpanKind = "client"const call = Effect.succeed(1).pipe(Effect.withSpan("api-call", { kind }))// => span "api-call" with kind=clientTracer.SpanLink
Section titled “Tracer.SpanLink”A relationship from one span to another, with descriptive attributes. Supplied
through the links option or Effect.linkSpans.
import { Tracer } from "effect"import type { Tracer as T } from "effect"
const link: T.SpanLink = { span: Tracer.externalSpan({ spanId: "s2", traceId: "t2" }), attributes: { "link.type": "follows-from" }}// => attach via Effect.withSpan("op", { links: [link] })Tracer.SpanOptions / SpanOptionsNoTrace / TraceOptions
Section titled “Tracer.SpanOptions / SpanOptionsNoTrace / TraceOptions”SpanOptions is the full options bag accepted by Effect.withSpan. It extends
SpanOptionsNoTrace (attributes, links, parent, root, annotations, kind,
sampled, level) with TraceOptions (captureStackTrace). The split exists
because some APIs (makeSpan, makeSpanScoped, useSpan) take only the
no-trace subset.
import type { Tracer } from "effect"
const options: Tracer.SpanOptions = { attributes: { "user.id": "123" }, kind: "internal", root: false, captureStackTrace: true}// => pass to Effect.withSpan("op", options)Tracer.ParentSpan and Tracer.ParentSpanKey
Section titled “Tracer.ParentSpan and Tracer.ParentSpanKey”ParentSpan is the context service holding the AnySpan used as the parent of
newly created spans, keyed by ParentSpanKey ("effect/Tracer/ParentSpan").
Effect.withParentSpan provides it; you can read it back to inspect the current
parent.
import { Effect, Tracer } from "effect"
const program = Effect.gen(function*() { const parent = yield* Effect.service(Tracer.ParentSpan) return parent.spanId})
console.log(Tracer.ParentSpanKey)// => "effect/Tracer/ParentSpan"Tracer.NativeSpan
Section titled “Tracer.NativeSpan”The default in-memory Span implementation used by the native tracer. It
generates random span ids (inheriting the parent’s trace id, or generating a new
one for a root), and stores attributes, events, and links in memory.
import { Option, Context } from "effect"import { Tracer } from "effect"
const span = new Tracer.NativeSpan({ name: "in-memory", parent: Option.none(), annotations: Context.empty(), links: [], startTime: 0n, kind: "internal", sampled: true})// => span.traceId is a fresh 32-char hex id, span.spanId a 16-char hex idTracer.CurrentTraceLevel
Section titled “Tracer.CurrentTraceLevel”A Context.Reference<LogLevel> (default "Info") giving the trace level used
for a span when its options don’t set level. Combined with
MinimumTraceLevel, it decides the default sampling of spans.
import { Effect, Tracer } from "effect"
const program = Effect.succeed(1).pipe( Effect.withSpan("debug-only"), Effect.provideService(Tracer.CurrentTraceLevel, "Debug"))// => the span is treated as Debug-level for gating purposesTracer.MinimumTraceLevel
Section titled “Tracer.MinimumTraceLevel”A Context.Reference<LogLevel> (default "All") setting the threshold below
which spans are not sampled. Spans whose level is below this threshold have their
sampling forced to false. An explicit sampled option bypasses this.
import { Effect, Tracer } from "effect"
const program = Effect.succeed(1).pipe( Effect.withSpan("trace-detail", { level: "Trace" }), // Only Info-and-above spans are sampled; this Trace span is dropped. Effect.provideService(Tracer.MinimumTraceLevel, "Info"))// => "trace-detail" is not exportedTracer.DisablePropagation
Section titled “Tracer.DisablePropagation”A Context.Reference<boolean> (default false). When true, new spans become
non-propagating no-ops and disabled spans are skipped when deriving a parent —
useful for suppressing tracing in noisy regions.
import { Effect, Tracer } from "effect"
const program = Effect.gen(function*() { yield* Effect.logInfo("not traced")}).pipe( Effect.withSpan("noop"), Effect.provideService(Tracer.DisablePropagation, true))// => no span is propagated out of this regionRelated
Section titled “Related”- Logging — correlate logs with spans via
Logger.tracerLogger. - Metrics — aggregate numeric telemetry.
- Exporters — full OTLP / Prometheus / OpenTelemetry wiring.
- Services & Layers — provide exporters as layers.