Logging
Logging in Effect is a first-class effect, not a side-effecting console.log.
Effect.logInfo, Effect.logError, and friends produce a log record that
carries the message, the level, the current fiber, a timestamp, any annotations
in scope, and any open log spans. What actually happens to that record — pretty
console output, one JSON line per entry, a file, or a remote collector — is
decided by the logger you install with a Layer. Your business code stays
the same regardless of destination.
import { Effect } from "effect"
// Logging functions are ordinary effects: yield them inside Effect.gen.const checkout = 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( // Attach structured metadata to every log line emitted by this effect… Effect.annotateLogs({ service: "checkout-api", route: "POST /checkout" }), // …and measure how long the whole flow takes (adds checkout=<N>ms). Effect.withLogSpan("checkout"))Because logging goes through the runtime, every record automatically picks up the fiber it ran on, the annotations and spans active at that point, and — when a tracer is installed — the current trace span. You never thread a “logger instance” through your call stack.
Log levels
Section titled “Log levels”Effect has six severities, ordered from most to least severe:
Fatal, Error, Warn, Info, Debug, Trace. Each has a dedicated
helper, and Effect.log defaults to Info. LogLevel also defines two
sentinel levels, All and None, used for filtering thresholds rather than
for emitting messages.
import { Effect } from "effect"
const program = Effect.gen(function*() { yield* Effect.log("default level is Info") yield* Effect.logFatal("the process cannot continue") yield* Effect.logError("an operation failed") yield* Effect.logWarning("something looks off") yield* Effect.logInfo("normal progress") yield* Effect.logDebug("detailed diagnostics") yield* Effect.logTrace("very fine-grained tracing")})Each helper accepts one or more values, so you can attach a structured payload directly to the message:
import { Effect } from "effect"
const handle = Effect.fn("handle")(function*(orderId: string) { // The object is rendered as structured metadata, not stringified into text. yield* Effect.logInfo("starting checkout", { orderId, attempt: 1 })})Filtering by minimum level
Section titled “Filtering by minimum level”A log record is only emitted if its severity meets the current minimum log
level, controlled by the References.MinimumLogLevel reference (default
Info, so Debug and Trace are dropped). Set it for the whole application
with a layer, or for a single effect with Effect.provideService.
import { Effect, Layer, References } from "effect"
// Application-wide: only Warn and above survive.const WarnAndAbove = Layer.succeed(References.MinimumLogLevel, "Warn")
// Per-effect: enable Debug logging for just this subtree.const verboseSection = Effect.gen(function*() { yield* Effect.logDebug("this now shows")}).pipe( Effect.provideService(References.MinimumLogLevel, "Debug"))Annotations and log spans
Section titled “Annotations and log spans”Annotations attach key/value context to every log record produced by an effect, and they nest: inner annotations merge with outer ones. Log spans record elapsed time, so the duration is appended to each log line emitted while the span is open.
import { Effect } from "effect"
const program = Effect.gen(function*() { yield* Effect.logInfo("request received") yield* Effect.sleep("75 millis") yield* Effect.logInfo("request handled")}).pipe( // Every line carries requestId and region… Effect.annotateLogs({ requestId: "req_42", region: "us-east-1" }), // …and is tagged with the elapsed time, e.g. http-request=78ms. Effect.withLogSpan("http-request"))This is far more useful than interpolating context into message strings: a
structured logger keeps requestId as a real field you can filter and search
on downstream.
Choosing a logger
Section titled “Choosing a logger”Effect ships several ready-made loggers. Install them with Logger.layer,
which replaces the default set (pass { mergeWithExisting: true } to add to
it instead).
import { Logger } from "effect"
// One JSON object per line — ideal for log collectors.const Json = Logger.layer([Logger.consoleJson])
// Human-readable, colorized output for local development.const Pretty = Logger.layer([Logger.consolePretty()])
// Structured fields rendered for the console.const Structured = Logger.layer([Logger.consoleStructured])
// logfmt key=value output.const LogFmt = Logger.layer([Logger.consoleLogFmt])The available console loggers are Logger.defaultLogger (the built-in pretty
format), Logger.consolePretty, Logger.consoleStructured,
Logger.consoleLogFmt, and Logger.consoleJson. Each is built from a
formatter (Logger.formatSimple, Logger.formatStructured,
Logger.formatLogFmt, Logger.formatJson) combined with an output sink.
Logging to a file
Section titled “Logging to a file”Logger.toFile turns a string formatter into a logger that batches output and
writes it to a path. It needs a FileSystem, so provide a platform layer.
import { NodeFileSystem } from "@effect/platform-node"import { Layer, Logger } from "effect"
const FileLogger = Logger.layer([ // Write one simple-formatted line per entry to app.log. Logger.toFile(Logger.formatSimple, "app.log")]).pipe( Layer.provide(NodeFileSystem.layer))Custom and batched loggers
Section titled “Custom and batched loggers”For app-specific routing — shipping logs to an external service, rotating
files, or coalescing writes — build your own logger. Logger.batched wraps any
formatter and flushes accumulated records on a time window, which keeps
high-frequency logging from overwhelming a downstream sink.
import { Effect, Logger } from "effect"
// A logger that buffers structured records and flushes them once per second.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) { // In a real implementation, POST the batch to a logging service. yield* Effect.sync(() => console.log(`flushing ${batch.length} entries`)) }) })})
// Logger.layer accepts effects that produce loggers, not just plain loggers.export const AppLoggerLayer = Logger.layer([appLogger])To build a logger entirely from scratch, Logger.make gives you the raw record
(message, logLevel, cause, date, fiber, and so on) so you can format
and route it however you like.
Selecting a logger per environment
Section titled “Selecting a logger per environment”Because Logger.layer accepts effects, you can decide which logger to install
based on configuration. Layer.unwrap lets a layer be produced by an effect.
import { Config, Effect, Layer, Logger } from "effect"
export const LoggerLayer = Layer.unwrap( Effect.gen(function*() { const env = yield* Config.string("NODE_ENV").pipe( Config.withDefault("development") ) // JSON in production for collectors, pretty output locally. return env === "production" ? Logger.layer([Logger.consoleJson]) : Logger.layer([Logger.consolePretty()]) }))Related
Section titled “Related”- Tracing — correlate logs with distributed traces.
- Metrics — aggregate numeric telemetry.
- Configuration — drive logger selection from config.
- Services & Layers — how layers wire telemetry in.