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))See File System for how to obtain a FileSystem
layer on each platform.
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()]) }))Emitting logs — Effect reference
Section titled “Emitting logs — Effect reference”Every log helper lives on the Effect namespace and returns an
Effect<void>. The level helpers below are all thin wrappers over
Effect.logWithLevel.
Effect.log
Section titled “Effect.log”Logs one or more values at the default Info level.
import { Effect } from "effect"
Effect.log("Result:", 4)// => timestamp=... level=INFO fiber=#0 message="Result: 4"Effect.logFatal
Section titled “Effect.logFatal”Logs at the Fatal level — the system is unusable and needs immediate
attention.
import { Effect } from "effect"
Effect.logFatal("event loop is wedged")// => timestamp=... level=FATAL fiber=#0 message="event loop is wedged"Effect.logError
Section titled “Effect.logError”Logs at the Error level for failures that should be investigated.
import { Effect } from "effect"
Effect.logError("payment provider timeout")// => timestamp=... level=ERROR fiber=#0 message="payment provider timeout"Effect.logWarning
Section titled “Effect.logWarning”Logs at the Warn level for conditions that may indicate a problem.
import { Effect } from "effect"
Effect.logWarning("inventory is low")// => timestamp=... level=WARN fiber=#0 message="inventory is low"Effect.logInfo
Section titled “Effect.logInfo”Logs at the Info level for normal-operation messages.
import { Effect } from "effect"
Effect.logInfo("user logged in", { userId: 123 })// => timestamp=... level=INFO fiber=#0 message="user logged in" userId=123Effect.logDebug
Section titled “Effect.logDebug”Logs at the Debug level. Suppressed by default, since the default minimum log
level is Info.
import { Effect } from "effect"
Effect.logDebug("cache miss for key user:123")// => (dropped unless MinimumLogLevel <= Debug)Effect.logTrace
Section titled “Effect.logTrace”Logs at the Trace level — the most verbose. Also suppressed by default.
import { Effect } from "effect"
Effect.logTrace("entering retry loop, attempt 1")// => (dropped unless MinimumLogLevel <= Trace)Effect.logWithLevel
Section titled “Effect.logWithLevel”Logs at a level chosen at runtime. Pass a LogLevel.Severity to get a logging
function; calling it with no argument is exactly Effect.log.
import { Effect, LogLevel } from "effect"
const emit = (level: LogLevel.Severity, msg: string) => Effect.logWithLevel(level)(msg)
emit("Warn", "disk almost full")// => timestamp=... level=WARN fiber=#0 message="disk almost full"Effect.annotateLogs
Section titled “Effect.annotateLogs”Attaches key/value context to every log record produced by the effect. Supports
both a single key, value pair and a Record of annotations; annotations nest
and merge with outer ones.
import { Effect } from "effect"
const work = Effect.logInfo("processing")
// Record formwork.pipe(Effect.annotateLogs({ userId: "u1", op: "data-processing" }))// => ... message="processing" userId=u1 op=data-processing
// key/value formwork.pipe(Effect.annotateLogs("requestId", "req-456"))// => ... message="processing" requestId=req-456Effect.annotateLogsScoped
Section titled “Effect.annotateLogsScoped”Adds annotations to the current Scope rather than to a single effect. The
annotations apply to every log line until the scope closes, then the previous
annotations are restored. Requires a Scope.
import { Effect } from "effect"
Effect.scoped( Effect.gen(function*() { yield* Effect.log("before") yield* Effect.annotateLogsScoped({ requestId: "req-123" }) yield* Effect.log("inside scope") // => ... requestId=req-123 }))Effect.withLogSpan
Section titled “Effect.withLogSpan”Opens a named timing span. Each log line emitted while the span is open gains a
<label>=<ms>ms field measuring the elapsed time since the span started. Spans
nest, so inner log lines carry both their own and any enclosing spans.
import { Effect } from "effect"
Effect.gen(function*() { yield* Effect.sleep("50 millis") yield* Effect.log("done")}).pipe(Effect.withLogSpan("task"))// => ... message="done" task=50msEffect.withLogger
Section titled “Effect.withLogger”Overrides the logger for one region of the program, adding the given logger to the active set for the wrapped effect only.
import { Effect, Logger } from "effect"
const audit = Logger.make((options) => console.log(`[AUDIT] ${options.message}`))
Effect.logInfo("sensitive action").pipe(Effect.withLogger(audit))// => [AUDIT] sensitive actionLog levels — LogLevel reference
Section titled “Log levels — LogLevel reference”The LogLevel module defines the severity model and the comparison helpers
used for filtering. Severities are ordered Fatal (most severe) down to
Trace (least severe), with All and None as sentinel bounds.
LogLevel.LogLevel
Section titled “LogLevel.LogLevel”The full union of level strings, including the All and None sentinels.
import type { LogLevel } from "effect"
const level: LogLevel.LogLevel = "Error"// type = "All" | "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" | "None"LogLevel.Severity
Section titled “LogLevel.Severity”The subset of concrete, emittable severities — excludes All and None. Use
it when only real message levels are valid (e.g. Effect.logWithLevel).
import type { LogLevel } from "effect"
const severity: LogLevel.Severity = "Warn"// type = "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace"LogLevel.values
Section titled “LogLevel.values”The runtime array of every level, in severity order from All through the
concrete levels to None.
import { LogLevel } from "effect"
console.log(LogLevel.values)// => ["All", "Fatal", "Error", "Warn", "Info", "Debug", "Trace", "None"]LogLevel.Order
Section titled “LogLevel.Order”An Order<LogLevel> over severity. Useful with Array.sort and the Order
combinators.
import { LogLevel } from "effect"
console.log(LogLevel.Order("Error", "Info")) // => 1 (Error more severe)console.log(LogLevel.Order("Debug", "Error")) // => -1console.log(LogLevel.Order("Info", "Info")) // => 0LogLevel.Equivalence
Section titled “LogLevel.Equivalence”Strict (===) equivalence between levels; each level only matches itself.
import { LogLevel } from "effect"
console.log(LogLevel.Equivalence("Error", "Error")) // => trueconsole.log(LogLevel.Equivalence("Error", "Info")) // => falseLogLevel.getOrdinal
Section titled “LogLevel.getOrdinal”Projects a level to its internal numeric sort key (Trace → 0, Debug →
10000, … Fatal → 50000, with All/None as the safe-integer bounds).
Treat these as sort keys, not external severity numbers.
import { LogLevel } from "effect"
console.log(LogLevel.getOrdinal("Info")) // => 20000console.log(LogLevel.getOrdinal("Error")) // => 40000LogLevel.isGreaterThan
Section titled “LogLevel.isGreaterThan”true when the first level is strictly more severe than the second. Curried
and data-first forms are both available.
import { LogLevel } from "effect"
console.log(LogLevel.isGreaterThan("Error", "Info")) // => trueconsole.log(LogLevel.isGreaterThan("Debug", "Error")) // => falseLogLevel.isGreaterThanOrEqualTo
Section titled “LogLevel.isGreaterThanOrEqualTo”true when the first level meets or exceeds the second — the core of
minimum-level filtering.
import { LogLevel } from "effect"
console.log(LogLevel.isGreaterThanOrEqualTo("Error", "Info")) // => trueconsole.log(LogLevel.isGreaterThanOrEqualTo("Debug", "Info")) // => falseLogLevel.isLessThan
Section titled “LogLevel.isLessThan”true when the first level is strictly less severe (more verbose) than the
second.
import { LogLevel } from "effect"
console.log(LogLevel.isLessThan("Trace", "Info")) // => trueconsole.log(LogLevel.isLessThan("Error", "Info")) // => falseLogLevel.isLessThanOrEqualTo
Section titled “LogLevel.isLessThanOrEqualTo”true when the first level is at or below the second — useful for maximum-level
(suppress-verbose) filtering.
import { LogLevel } from "effect"
console.log(LogLevel.isLessThanOrEqualTo("Debug", "Info")) // => trueconsole.log(LogLevel.isLessThanOrEqualTo("Error", "Info")) // => falseLogLevel.isEnabled
Section titled “LogLevel.isEnabled”An effect that reports whether a level would currently be emitted, by reading
the fiber’s References.MinimumLogLevel.
import { Effect, LogLevel, References } from "effect"
Effect.gen(function*() { const debug = yield* LogLevel.isEnabled("Debug") // => false at default Info const error = yield* LogLevel.isEnabled("Error") // => true console.log({ debug, error })}).pipe(Effect.provideService(References.MinimumLogLevel, "Info"))Loggers — Logger reference
Section titled “Loggers — Logger reference”A Logger<Message, Output> receives each runtime log event (as Options) and
turns it into an output: a string, a structured object, a console write, a file
write, or a trace span event.
Logger.Logger
Section titled “Logger.Logger”The logger interface. Its single log(options) method maps a log event to
Output. Loggers are plain values; install them with Logger.layer.
import type { Logger } from "effect"
// A logger that consumes any message and produces a string.declare const fmt: Logger.Logger<unknown, string>Logger.Options
Section titled “Logger.Options”The per-event record passed to a logger: message, logLevel, cause,
fiber, and date. Read annotations/spans off the fiber via
fiber.getRef(References.CurrentLogAnnotations) / CurrentLogSpans.
import { Logger } from "effect"
const logger = Logger.make((options /* Logger.Options<unknown> */) => { console.log(options.logLevel, options.date.toISOString(), options.message)})// => Info 2025-01-03T14:22:47.570Z helloLogger.isLogger
Section titled “Logger.isLogger”Type guard for logger values.
import { Logger } from "effect"
const l = Logger.make(() => {})console.log(Logger.isLogger(l)) // => trueconsole.log(Logger.isLogger("nope")) // => falseLogger.make
Section titled “Logger.make”Creates a logger from a function over Options. Return whatever output type you
need; return undefined (or simply don’t write) to skip an event.
import { Logger } from "effect"
const textLogger = Logger.make((options) => `${options.date.toISOString()} [${options.logLevel}] ${options.message}`)// textLogger.log(...) => "2025-01-03T... [Info] hello"Logger.map
Section titled “Logger.map”Transforms a logger’s output without rewriting its logic — e.g. wrap a JSON string in an envelope.
import { Logger } from "effect"
const enveloped = Logger.map( Logger.formatJson, (json) => `{"service":"api","entry":${json}}`)// enveloped.log(...) => {"service":"api","entry":{"message":["hi"],...}}Logger.defaultLogger
Section titled “Logger.defaultLogger”The runtime’s built-in logger (pretty console output). Installed by default
alongside tracerLogger.
import { Effect, Logger } from "effect"
Effect.log("hello").pipe( Effect.provide(Logger.layer([Logger.defaultLogger])))// => [09:37:17.579] INFO (#0): helloLogger.formatSimple
Section titled “Logger.formatSimple”Formatter producing a single-line string with double-quoted values. Output only — wrap it with a sink to write it.
import { Logger } from "effect"
Logger.formatSimple.log// => timestamp=2025-01-03T14:22:47.570Z level=Info fiber=#1 message=helloLogger.formatLogFmt
Section titled “Logger.formatLogFmt”Formatter producing logfmt key=value output
(JSON-stringified values). Output only.
import { Logger } from "effect"
Logger.formatLogFmt.log// => timestamp=2025-01-03T14:22:47.570Z level=Info fiber=#1 message=helloLogger.formatStructured
Section titled “Logger.formatStructured”Formatter producing a structured object with message, level, timestamp,
cause, annotations, spans, and fiberId fields. Output only.
import { Logger } from "effect"
Logger.formatStructured.log// => { message: "hello", level: "INFO", timestamp: "2025-01-03T...",// cause: undefined, annotations: { key: "value" },// spans: { label: 0 }, fiberId: "#1" }Logger.formatJson
Section titled “Logger.formatJson”formatStructured serialized to a single JSON line (Logger.map over the
structured formatter). Output only.
import { Logger } from "effect"
Logger.formatJson.log// => {"message":["hello"],"level":"INFO","timestamp":"2025-01-03T...",// "annotations":{"key":"value"},"spans":{"label":0},"fiberId":"#1"}Logger.withConsoleLog
Section titled “Logger.withConsoleLog”Wraps a formatter so its output is written with console.log.
import { Logger } from "effect"
const consoleSimple = Logger.withConsoleLog(Logger.formatSimple)// installing this writes each formatted line via console.logLogger.withConsoleError
Section titled “Logger.withConsoleError”Like withConsoleLog, but writes with console.error (stderr).
import { Logger } from "effect"
const errToStderr = Logger.withConsoleError(Logger.formatLogFmt)// each line goes to console.errorLogger.withLeveledConsole
Section titled “Logger.withLeveledConsole”Wraps a formatter so each record is written with the console method matching its
level: Debug → console.debug, Info → console.info, Trace →
console.trace, Warn → console.warn, Error/Fatal → console.error,
otherwise console.log.
import { Logger } from "effect"
const leveled = Logger.withLeveledConsole(Logger.formatSimple)// Effect.logWarning(...) => console.warn(...); Effect.logError(...) => console.error(...)Logger.consolePretty
Section titled “Logger.consolePretty”Pretty, optionally colorized console logger. Takes options for colors,
stderr, mode ("browser" | "tty" | "auto"), and formatDate.
import { Logger } from "effect"
Logger.consolePretty({ colors: true })// => [09:37:17.579] INFO (#1) label=0ms: hello// key: valueLogger.consoleLogFmt
Section titled “Logger.consoleLogFmt”logfmt formatter routed to console.log (withConsoleLog(formatLogFmt)).
import { Logger } from "effect"
Logger.consoleLogFmt// console.log => timestamp=2025-01-03T... level=INFO fiber=#1 message=infoLogger.consoleStructured
Section titled “Logger.consoleStructured”Structured formatter routed to console.log (withConsoleLog(formatStructured)).
import { Logger } from "effect"
Logger.consoleStructured// console.log => { message: ["info","message"], level: "INFO", ... }Logger.consoleJson
Section titled “Logger.consoleJson”JSON formatter routed to console.log (withConsoleLog(formatJson)). The
common choice for containerized/aggregated logs.
import { Logger } from "effect"
Logger.consoleJson// console.log => {"message":["hello"],"level":"INFO","timestamp":"...","fiberId":"#1"}Logger.tracerLogger
Section titled “Logger.tracerLogger”Records each log message as an event on the current trace span. Included in the default logger set, so logs automatically appear on spans unless you replace the loggers without merging. See Tracing.
import { Effect, Logger } from "effect"
Effect.log("step 1").pipe( Effect.withLogSpan("workflow"), Effect.provide(Logger.layer([Logger.tracerLogger, Logger.consoleJson])))// the "step 1" log becomes a span event on "workflow"Logger.batched
Section titled “Logger.batched”Wraps a formatter into a scoped logger that buffers output and periodically
hands the buffer to flush. Remaining entries are flushed when the scope
closes. Returns an Effect (it forks a background flusher), so provide it
through Logger.layer([...]).
import { Duration, Effect, Logger } from "effect"
const batched = Logger.batched(Logger.formatJson, { window: Duration.seconds(5), flush: (lines) => Effect.sync(() => console.log(`flushing ${lines.length} entries`))})// => flushing 3 entries (every 5s, plus once on scope close)Logger.toFile
Section titled “Logger.toFile”Turns a string logger into a scoped logger that batches output and writes it
to a file path. Requires FileSystem and Scope; pair it with a string
formatter (formatJson, formatLogFmt, formatSimple). Options: flag,
mode, batchWindow.
import { NodeFileSystem } from "@effect/platform-node"import { Duration, Layer, Logger } from "effect"
const FileLogger = Logger.layer([ Logger.formatLogFmt.pipe( Logger.toFile("/var/log/app.log", { flag: "a", batchWindow: Duration.seconds(5) }) )]).pipe(Layer.provide(NodeFileSystem.layer))// appends batched logfmt lines to /var/log/app.logLogger.layer
Section titled “Logger.layer”Builds a Layer that installs the given loggers. Replaces the current set by
default; pass { mergeWithExisting: true } to add to it. Accepts both plain
loggers and effects that produce loggers (such as batched/toFile results).
import { Logger } from "effect"
// Replace the runtime loggers with JSON-only output.Logger.layer([Logger.consoleJson])
// Add a logger while keeping the defaults (incl. tracerLogger).Logger.layer([Logger.consolePretty()], { mergeWithExisting: true })Logger.CurrentLoggers
Section titled “Logger.CurrentLoggers”The Context.Reference holding the active logger set for the current fiber.
Read it to inspect, or provide it (which is what Logger.layer does under the
hood).
import { Effect, Logger } from "effect"
Effect.gen(function*() { const loggers = yield* Effect.service(Logger.CurrentLoggers) console.log(`active loggers: ${loggers.size}`) // => active loggers: 2})Logger.LogToStderr
Section titled “Logger.LogToStderr”A Context.Reference<boolean> (default false). Set it to true to route the
default logger and the TTY pretty logger to console.error, keeping stdout
clean for protocol/data output.
import { Effect, Layer, Logger } from "effect"
const StderrLogs = Layer.succeed(Logger.LogToStderr, true)
Effect.log("goes to stderr").pipe(Effect.provide(StderrLogs))Related
Section titled “Related”- Tracing — correlate logs with distributed traces
via
Logger.tracerLogger. - Metrics — aggregate numeric telemetry.
- File System — the
FileSystemlayer needed byLogger.toFile. - Configuration — drive logger selection from config.
- Services & Layers — how layers wire telemetry in.