# 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.

```ts
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

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.

```ts
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:

```ts
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

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`.

```ts
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")
)
```
**Note:** The minimum log level only filters what loggers *emit* — the log effects still
run. Use it to turn the volume up or down per environment without editing call
sites.

## 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.

```ts
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

Effect ships several ready-made loggers. Install them with `Logger.layer`,
which *replaces* the default set (pass `{ mergeWithExisting: true }` to add to
it instead).

```ts
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

`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.

```ts
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](https://effect.plants.sh/platform/file-system/) for how to obtain a `FileSystem`
layer on each platform.

## 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.

```ts
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

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.

```ts
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()])
  })
)
```
**Tip:** To forward log records into your traces as span events, add
`Logger.tracerLogger` to the logger set. Each log line then appears on the
active span — see [Tracing](https://effect.plants.sh/observability/tracing/).

## 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`

Logs one or more values at the default `Info` level.

```ts
import { Effect } from "effect"

Effect.log("Result:", 4)
// => timestamp=... level=INFO fiber=#0 message="Result: 4"
```

### `Effect.logFatal`

Logs at the `Fatal` level — the system is unusable and needs immediate
attention.

```ts
import { Effect } from "effect"

Effect.logFatal("event loop is wedged")
// => timestamp=... level=FATAL fiber=#0 message="event loop is wedged"
```

### `Effect.logError`

Logs at the `Error` level for failures that should be investigated.

```ts
import { Effect } from "effect"

Effect.logError("payment provider timeout")
// => timestamp=... level=ERROR fiber=#0 message="payment provider timeout"
```

### `Effect.logWarning`

Logs at the `Warn` level for conditions that may indicate a problem.

```ts
import { Effect } from "effect"

Effect.logWarning("inventory is low")
// => timestamp=... level=WARN fiber=#0 message="inventory is low"
```

### `Effect.logInfo`

Logs at the `Info` level for normal-operation messages.

```ts
import { Effect } from "effect"

Effect.logInfo("user logged in", { userId: 123 })
// => timestamp=... level=INFO fiber=#0 message="user logged in" userId=123
```

### `Effect.logDebug`

Logs at the `Debug` level. Suppressed by default, since the default minimum log
level is `Info`.

```ts
import { Effect } from "effect"

Effect.logDebug("cache miss for key user:123")
// => (dropped unless MinimumLogLevel <= Debug)
```

### `Effect.logTrace`

Logs at the `Trace` level — the most verbose. Also suppressed by default.

```ts
import { Effect } from "effect"

Effect.logTrace("entering retry loop, attempt 1")
// => (dropped unless MinimumLogLevel <= Trace)
```

### `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`.

```ts
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`

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.

```ts
import { Effect } from "effect"

const work = Effect.logInfo("processing")

// Record form
work.pipe(Effect.annotateLogs({ userId: "u1", op: "data-processing" }))
// => ... message="processing" userId=u1 op=data-processing

// key/value form
work.pipe(Effect.annotateLogs("requestId", "req-456"))
// => ... message="processing" requestId=req-456
```

### `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`.

```ts
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`

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.

```ts
import { Effect } from "effect"

Effect.gen(function*() {
  yield* Effect.sleep("50 millis")
  yield* Effect.log("done")
}).pipe(Effect.withLogSpan("task"))
// => ... message="done" task=50ms
```

### `Effect.withLogger`

Overrides the logger for one region of the program, adding the given logger to
the active set for the wrapped effect only.

```ts
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 action
```

## 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`

The full union of level strings, including the `All` and `None` sentinels.

```ts
import type { LogLevel } from "effect"

const level: LogLevel.LogLevel = "Error"
// type = "All" | "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" | "None"
```

### `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`).

```ts
import type { LogLevel } from "effect"

const severity: LogLevel.Severity = "Warn"
// type = "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace"
```

### `LogLevel.values`

The runtime array of every level, in severity order from `All` through the
concrete levels to `None`.

```ts
import { LogLevel } from "effect"

console.log(LogLevel.values)
// => ["All", "Fatal", "Error", "Warn", "Info", "Debug", "Trace", "None"]
```

### `LogLevel.Order`

An `Order<LogLevel>` over severity. Useful with `Array.sort` and the `Order`
combinators.

```ts
import { LogLevel } from "effect"

console.log(LogLevel.Order("Error", "Info")) // => 1  (Error more severe)
console.log(LogLevel.Order("Debug", "Error")) // => -1
console.log(LogLevel.Order("Info", "Info")) // => 0
```

### `LogLevel.Equivalence`

Strict (`===`) equivalence between levels; each level only matches itself.

```ts
import { LogLevel } from "effect"

console.log(LogLevel.Equivalence("Error", "Error")) // => true
console.log(LogLevel.Equivalence("Error", "Info")) // => false
```

### `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.

```ts
import { LogLevel } from "effect"

console.log(LogLevel.getOrdinal("Info")) // => 20000
console.log(LogLevel.getOrdinal("Error")) // => 40000
```

### `LogLevel.isGreaterThan`

`true` when the first level is strictly more severe than the second. Curried
and data-first forms are both available.

```ts
import { LogLevel } from "effect"

console.log(LogLevel.isGreaterThan("Error", "Info")) // => true
console.log(LogLevel.isGreaterThan("Debug", "Error")) // => false
```

### `LogLevel.isGreaterThanOrEqualTo`

`true` when the first level meets or exceeds the second — the core of
minimum-level filtering.

```ts
import { LogLevel } from "effect"

console.log(LogLevel.isGreaterThanOrEqualTo("Error", "Info")) // => true
console.log(LogLevel.isGreaterThanOrEqualTo("Debug", "Info")) // => false
```

### `LogLevel.isLessThan`

`true` when the first level is strictly less severe (more verbose) than the
second.

```ts
import { LogLevel } from "effect"

console.log(LogLevel.isLessThan("Trace", "Info")) // => true
console.log(LogLevel.isLessThan("Error", "Info")) // => false
```

### `LogLevel.isLessThanOrEqualTo`

`true` when the first level is at or below the second — useful for maximum-level
(suppress-verbose) filtering.

```ts
import { LogLevel } from "effect"

console.log(LogLevel.isLessThanOrEqualTo("Debug", "Info")) // => true
console.log(LogLevel.isLessThanOrEqualTo("Error", "Info")) // => false
```

### `LogLevel.isEnabled`

An effect that reports whether a level would currently be emitted, by reading
the fiber's `References.MinimumLogLevel`.

```ts
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

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`

The logger interface. Its single `log(options)` method maps a log event to
`Output`. Loggers are plain values; install them with `Logger.layer`.

```ts
import type { Logger } from "effect"

// A logger that consumes any message and produces a string.
declare const fmt: Logger.Logger<unknown, string>
```

### `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`.

```ts
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 hello
```

### `Logger.isLogger`

Type guard for logger values.

```ts
import { Logger } from "effect"

const l = Logger.make(() => {})
console.log(Logger.isLogger(l)) // => true
console.log(Logger.isLogger("nope")) // => false
```

### `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.

```ts
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`

Transforms a logger's output without rewriting its logic — e.g. wrap a JSON
string in an envelope.

```ts
import { Logger } from "effect"

const enveloped = Logger.map(
  Logger.formatJson,
  (json) => `{"service":"api","entry":${json}}`
)
// enveloped.log(...) => {"service":"api","entry":{"message":["hi"],...}}
```

### `Logger.defaultLogger`

The runtime's built-in logger (pretty console output). Installed by default
alongside `tracerLogger`.

```ts
import { Effect, Logger } from "effect"

Effect.log("hello").pipe(
  Effect.provide(Logger.layer([Logger.defaultLogger]))
)
// => [09:37:17.579] INFO (#0): hello
```

### `Logger.formatSimple`

Formatter producing a single-line string with double-quoted values. Output only
— wrap it with a sink to write it.

```ts
import { Logger } from "effect"

Logger.formatSimple.log
// => timestamp=2025-01-03T14:22:47.570Z level=Info fiber=#1 message=hello
```

### `Logger.formatLogFmt`

Formatter producing [logfmt](https://brandur.org/logfmt) `key=value` output
(JSON-stringified values). Output only.

```ts
import { Logger } from "effect"

Logger.formatLogFmt.log
// => timestamp=2025-01-03T14:22:47.570Z level=Info fiber=#1 message=hello
```

### `Logger.formatStructured`

Formatter producing a structured object with `message`, `level`, `timestamp`,
`cause`, `annotations`, `spans`, and `fiberId` fields. Output only.

```ts
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`

`formatStructured` serialized to a single JSON line (`Logger.map` over the
structured formatter). Output only.

```ts
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`

Wraps a formatter so its output is written with `console.log`.

```ts
import { Logger } from "effect"

const consoleSimple = Logger.withConsoleLog(Logger.formatSimple)
// installing this writes each formatted line via console.log
```

### `Logger.withConsoleError`

Like `withConsoleLog`, but writes with `console.error` (stderr).

```ts
import { Logger } from "effect"

const errToStderr = Logger.withConsoleError(Logger.formatLogFmt)
// each line goes to console.error
```

### `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`.

```ts
import { Logger } from "effect"

const leveled = Logger.withLeveledConsole(Logger.formatSimple)
// Effect.logWarning(...) => console.warn(...); Effect.logError(...) => console.error(...)
```

### `Logger.consolePretty`

Pretty, optionally colorized console logger. Takes options for `colors`,
`stderr`, `mode` (`"browser" | "tty" | "auto"`), and `formatDate`.

```ts
import { Logger } from "effect"

Logger.consolePretty({ colors: true })
// => [09:37:17.579] INFO (#1) label=0ms: hello
//      key: value
```

### `Logger.consoleLogFmt`

logfmt formatter routed to `console.log` (`withConsoleLog(formatLogFmt)`).

```ts
import { Logger } from "effect"

Logger.consoleLogFmt
// console.log => timestamp=2025-01-03T... level=INFO fiber=#1 message=info
```

### `Logger.consoleStructured`

Structured formatter routed to `console.log` (`withConsoleLog(formatStructured)`).

```ts
import { Logger } from "effect"

Logger.consoleStructured
// console.log => { message: ["info","message"], level: "INFO", ... }
```

### `Logger.consoleJson`

JSON formatter routed to `console.log` (`withConsoleLog(formatJson)`). The
common choice for containerized/aggregated logs.

```ts
import { Logger } from "effect"

Logger.consoleJson
// console.log => {"message":["hello"],"level":"INFO","timestamp":"...","fiberId":"#1"}
```

### `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](https://effect.plants.sh/observability/tracing/).

```ts
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`

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([...])`.

```ts
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`

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`.

```ts
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.log
```

### `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).

```ts
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`

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).

```ts
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`

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.

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

const StderrLogs = Layer.succeed(Logger.LogToStderr, true)

Effect.log("goes to stderr").pipe(Effect.provide(StderrLogs))
```

## Related

- [Tracing](https://effect.plants.sh/observability/tracing/) — correlate logs with distributed traces
  via `Logger.tracerLogger`.
- [Metrics](https://effect.plants.sh/observability/metrics/) — aggregate numeric telemetry.
- [File System](https://effect.plants.sh/platform/file-system/) — the `FileSystem` layer needed by
  `Logger.toFile`.
- [Configuration](https://effect.plants.sh/configuration/) — drive logger selection from config.
- [Services & Layers](https://effect.plants.sh/services-and-layers/) — how layers wire telemetry in.