Observability

Effect has built-in structured logging, distributed tracing, and metrics. For telemetry export, use the lightweight Otlp modules from effect/unstable/observability in new projects, or @effect/opentelemetry NodeSdk when integrating with an existing OpenTelemetry setup.

Structured Logging

import { Effect } from "effect"

const program = Effect.gen(function*() {
  yield* Effect.logDebug("loading checkout state")
  yield* Effect.logInfo("validating cart")
  yield* Effect.logWarning("inventory is low for one line item")
  yield* Effect.logError("payment provider timeout")
}).pipe(
  Effect.annotateLogs({
    service: "checkout-api",
    route: "POST /checkout"
  }),
  Effect.withLogSpan("checkout")
)
FunctionLevelUse case
Effect.logInfoGeneral messages
Effect.logInfoInfoInformational events
Effect.logDebugDebugDiagnostic detail
Effect.logWarningWarningSomething unexpected
Effect.logErrorErrorFailures

Effect.annotateLogs attaches structured metadata to all log lines emitted by an effect. Effect.withLogSpan adds a named duration measurement to each log line.

Log Level Filtering

Control the minimum log level via References.MinimumLogLevel:

import { Layer, References } from "effect"

const WarnAndAbove = Layer.succeed(References.MinimumLogLevel, "Warn")

Custom Loggers

import { Effect, Layer, Logger } from "effect"

const JsonLoggerLayer = Logger.layer([Logger.consoleJson])

const FileLoggerLayer = Logger.layer([
  Logger.toFile(Logger.formatSimple, "app.log")
]).pipe(
  Layer.provide(NodeFileSystem.layer)
)

For a custom logger with batching:

import { Effect, Logger } from "effect"

const appLogger = Effect.gen(function*() {
  yield* Effect.logDebug("initializing app logger")
  return yield* Logger.batched(Logger.formatStructured, {
    window: "1 second",
    flush: Effect.fn(function*(batch) {
      console.log(`Flushing ${batch.length} log entries`)
    })
  })
})

Tip: Use Layer.unwrap to choose loggers dynamically based on configuration. Build a dev logger for development and a JSON/batched logger for production.

Distributed Tracing

Automatic Spans with Effect.fn

Passing a string to Effect.fn attaches a tracing span automatically (using Effect.withSpan behind the scenes). The name should match the function name.

import { Effect } from "effect"

export const processCheckout = Effect.fn("processCheckout")(function*(orderId: string) {
  yield* Effect.logInfo("starting checkout", { orderId })

  yield* Effect.sleep("50 millis").pipe(
    Effect.withSpan("checkout.charge-card"),
    Effect.annotateSpans({
      "checkout.order_id": orderId,
      "checkout.provider": "acme-pay"
    })
  )

  yield* Effect.sleep("20 millis").pipe(
    Effect.withSpan("checkout.persist-order")
  )

  yield* Effect.logInfo("checkout completed", { orderId })
})

Manual Spans

import { Effect } from "effect"

const task = Effect.succeed("result").pipe(
  Effect.withSpan("my-task", {
    attributes: { key: "value" }
  })
)

Span Annotations

Effect.annotateSpans and Effect.annotateCurrentSpan attach key-value attributes to the current span. These appear in your tracing backend (Jaeger, Tempo, etc.) and help with filtering and correlation.

Gotcha: The span name from Effect.fn("name") improves stack traces too. Always pass the function name as the string argument.

Metrics

import { Effect, Metric } from "effect"

const requestCounter = Metric.counter("http.requests.total")
const activeGauge = Metric.gauge("http.connections.active")
const latencyHistogram = Metric.histogram("http.request.duration", {
  buckets: [10, 50, 100, 500, 1000]
})

const handleRequest = Effect.fn(function*() {
  yield* Metric.increment(requestCounter)
  yield* Metric.set(activeGauge, 42)

  const start = Date.now()
  yield* doWork()
  yield* Metric.update(latencyHistogram, Date.now() - start)
})
Metric typeAPIUse case
CounterMetric.counter(name)Monotonic counts (requests, errors)
GaugeMetric.gauge(name)Current value (connections, queue depth)
HistogramMetric.histogram(name, { buckets })Distributions (latency, payload size)

Use Effect.track to automatically track metrics on an effect:

const tracked = handleRequest.pipe(
  Effect.track(requestCounter),
  Effect.track(latencyHistogram)
)

OpenTelemetry Integration

Otlp Modules (v4 New Projects)

The Otlp modules live under effect/unstable/observability. They provide lightweight OTLP export without requiring the full @effect/opentelemetry package.

import { Context, Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability"

const OtlpTracingLayer = OtlpTracer.layer({
  url: "http://localhost:4318/v1/traces",
  resource: {
    serviceName: "checkout-api",
    serviceVersion: "1.0.0",
    attributes: { "deployment.environment": "staging" }
  }
})

const OtlpLoggingLayer = OtlpLogger.layer({
  url: "http://localhost:4318/v1/logs",
  resource: {
    serviceName: "checkout-api",
    serviceVersion: "1.0.0"
  }
})

export const ObservabilityLayer = Layer.merge(OtlpTracingLayer, OtlpLoggingLayer).pipe(
  Layer.provide(OtlpSerialization.layerJson),
  Layer.provide(FetchHttpClient.layer)
)

Provide the observability layer at the very end so all spans created by the app are exported:

import { Layer } from "effect"

const Main = AppLayer.pipe(
  Layer.provide(ObservabilityLayer)
)

Tip: You can attach spans to Layers too. Use Layer.withSpan("my-layer") to trace layer construction.

Existing @effect/opentelemetry Setups

If you already have an OpenTelemetry setup with @effect/opentelemetry NodeSdk, you can continue using it. The Otlp modules are an alternative, not a replacement. The @effect/opentelemetry package still works for existing integrations.

v4 Import Mapping Changes

Several observability-related imports moved in v4:

v3v4
@effect/opentelemetry/Otlpeffect/unstable/observability/Otlp
@effect/opentelemetry/OtlpTracereffect/unstable/observability/OtlpTracer
@effect/opentelemetry/OtlpLoggereffect/unstable/observability/OtlpLogger
@effect/opentelemetry/OtlpMetricseffect/unstable/observability/OtlpMetrics
@effect/opentelemetry/OtlpSerializationeffect/unstable/observability/OtlpSerialization
effect/TestClockeffect/testing/TestClock
effect/FiberRefeffect/References
effect/Inspectableeffect/Redactable (for redaction concerns)

Gotcha: Logger, Metric, and Tracer stay in the core effect package. They did not move to unstable/. Only the Otlp export modules moved.