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 over OTLP
Section titled “Exporting traces over OTLP”By itself, Effect builds the span tree but does not send it anywhere. The
OtlpTracer module from effect/unstable/observability posts spans to any
OpenTelemetry-compatible collector. It needs two supporting layers: an OTLP
serializer (OtlpSerialization.layerJson) and an HttpClient to do the POSTing
(FetchHttpClient.layer).
import { Layer } from "effect"import { FetchHttpClient } from "effect/unstable/http"import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability"
// Export spans to a local OTLP collector.const OtlpTracing = OtlpTracer.layer({ url: "http://localhost:4318/v1/traces", resource: { serviceName: "checkout-api", serviceVersion: "1.0.0", attributes: { "deployment.environment": "staging" } }})
// Optionally export log records the same way, so logs and traces correlate.const OtlpLogging = OtlpLogger.layer({ url: "http://localhost:4318/v1/logs", resource: { serviceName: "checkout-api", serviceVersion: "1.0.0" }})
// A single reusable observability layer for the whole app.export const ObservabilityLayer = Layer.merge(OtlpTracing, OtlpLogging).pipe( // OtlpTracer/OtlpLogger require a serializer and an HttpClient. Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))The resource describes the service emitting the telemetry: serviceName and
serviceVersion populate the OTLP resource, and attributes adds arbitrary
resource-level tags. You can also tune exportInterval, maxBatchSize, and
pass headers (for example an auth token to a hosted collector).
Wiring it into an application
Section titled “Wiring it into an application”Provide the observability layer last, after your application layers, so that every span the app creates is captured before being exported.
import { NodeRuntime } from "@effect/platform-node"import { Effect, Layer } from "effect"
const Run = Layer.effectDiscard( Effect.gen(function*() { const checkout = yield* Checkout yield* checkout.processCheckout("ord_123") }).pipe(Effect.withSpan("checkout-test-run"))).pipe( Layer.provide(Checkout.layer), // Provide observability at the very end so all spans are exported. Layer.provide(ObservabilityLayer))
Layer.launch(Run).pipe(NodeRuntime.runMain)Related
Section titled “Related”- Logging — correlate logs with spans.
- Metrics — aggregate numeric telemetry.
- Services & Layers — provide exporters as layers.
- Platform —
HttpClientand runtime entry points.