Skip to content

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.

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.

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"))

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"))

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.

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.

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 42

SpanOptions (= 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 — when true, 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 by MinimumTraceLevel gating.
  • captureStackTrace?: boolean | (() => string | undefined) — capture (or supply) a stack trace for the span; set false to 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=server

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 ends

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)

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
})
)

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=123

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 t9

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)

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" only

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)

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)

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" }

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)

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/timed

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"

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.

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 NativeSpan

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"

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"

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 it

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 t1

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_000n

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=client

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"

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 id

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 purposes

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 exported

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 region
  • Logging — correlate logs with spans via Logger.tracerLogger.
  • Metrics — aggregate numeric telemetry.
  • Exporters — full OTLP / Prometheus / OpenTelemetry wiring.
  • Services & Layers — provide exporters as layers.