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")
)
| Function | Level | Use case |
|---|---|---|
Effect.log | Info | General messages |
Effect.logInfo | Info | Informational events |
Effect.logDebug | Debug | Diagnostic detail |
Effect.logWarning | Warning | Something unexpected |
Effect.logError | Error | Failures |
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.unwrapto 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 type | API | Use case |
|---|---|---|
| Counter | Metric.counter(name) | Monotonic counts (requests, errors) |
| Gauge | Metric.gauge(name) | Current value (connections, queue depth) |
| Histogram | Metric.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:
| v3 | v4 |
|---|---|
@effect/opentelemetry/Otlp | effect/unstable/observability/Otlp |
@effect/opentelemetry/OtlpTracer | effect/unstable/observability/OtlpTracer |
@effect/opentelemetry/OtlpLogger | effect/unstable/observability/OtlpLogger |
@effect/opentelemetry/OtlpMetrics | effect/unstable/observability/OtlpMetrics |
@effect/opentelemetry/OtlpSerialization | effect/unstable/observability/OtlpSerialization |
effect/TestClock | effect/testing/TestClock |
effect/FiberRef | effect/References |
effect/Inspectable | effect/Redactable (for redaction concerns) |
Gotcha:
Logger,Metric, andTracerstay in the coreeffectpackage. They did not move tounstable/. Only the Otlp export modules moved.