Logger
Logger module: structured logging with levels, scoped context, and pluggable appenders.
Log levels (ascending severity): Debug < Info < Warn < Error. The global level filter is set with set_level/1; messages below the current level are silently discarded.
v2 adds rich field values and scoped context:
Logger.with_field("user_id", Logger.i(42))
Logger.with_field("ms", Logger.f(12.5))
Logger.info("request done")The structured fields propagate to every subsequent log call until the field stack is popped. Use with_scope for automatic cleanup:
Logger.with_scope([Logger.LogField("request_id", Logger.s("abc"))],
fn _ -> handle_request())Output goes to stderr in the format: [LEVEL] message {key:value, key2:value2}
v1 API (still supported): Logger.set_level(Logger.Info) Logger.with_context("request_id", "abc123") -- equivalent to with_field("...", LStr(...)) Logger.info("handling request") Logger.warn("slow query") Logger.error("db connection lost") Logger.log_with(Logger.Info, "user signed in", [("user", "alice")])
Types
Functions
Register an appender. Replaces any prior appender with the same name.
A custom appender that calls the user's on_entry callback for each log entry. Use to forward entries to OpenTelemetry exporters, in-process telemetry buses, or test harnesses.
An appender that appends each formatted entry as a new line to path. Open / write / close happens per call (slow but simple); a buffered variant lives in a future commit.
An appender that writes formatted entries to stderr. Pass any formatter (e.g. Logger.format_json) to control wire format.
Logger.add_appender(Logger.appender_stderr(Logger.format_json))An appender that writes formatted entries to stdout.
Remove every registered appender. Future logs will fall back to the v1 stderr text format.
Remove all log fields (v1 + v2).
Remove a module-level override; the module reverts to the global level.
Return the current log context as a List(LogField), most-recent push at the head. Useful for inspecting state in tests or for copying the current context into another actor's mailbox.
Return the parent_span_id from the log context, if set.
Return the current span_id from the log context, if set.
Return the current trace_id from the log context, if set.
Log a Debug-level message.
Return the current depth of the field stack. Useful for tests.
Format a log entry as a simple string: [LEVEL] message
Single-line JSON formatter, OpenTelemetry-friendly.
Example output:
{"ts":1681400000000,"level":"info","msg":"login","src":"MyApp",
"trace_id":"…","span_id":"…","attributes":{"user_id":42}}
The reserved field names `trace_id`, `span_id`, and `parent_span_id`
appear at the top level of the JSON object so log shippers that
speak OTLP can pluck them directly; everything else lands in the
`attributes` sub-object.logfmt-style formatter (Heroku / Grafana convention): level=info ts=1681400000000 msg="hello world" k=v String values containing spaces or quotes are escaped.
Default text formatter. Mirrors the v1 output: [LEVEL] message {key:value, key2:value2}
Get the current minimum log level.
Log an Info-level message.
Return true if level is at least as severe as min_level.
Return the effective level for module — the override if one is set, otherwise the global default.
Parse a log level from a string (case-insensitive). Returns None for unknown strings.
Return the numeric rank of a log level: Debug=0, Info=1, Warn=2, Error=3.
Return the names of all currently registered appenders.
Log a message at the given level.
Log at level only if level is enabled relative to min_level.
Log a message scoped to module. The level is filtered by the per-module override (set via set_module_level), falling back to the global level if no override is registered. The module name appears as the source of the LogEntry — formatters that include source render it as src=… (logfmt) or "src": "…" (json).
Logger.log_in("MyApp.Worker", Logger.Debug, "task done",
[Logger.LogField("ms", Logger.i(42))])Log a message at the given level with extra metadata. metadata is a List((String, String)) of key-value pairs.
Logger.log_with(Logger.Info, "request done", [("status", "200"), ("ms", "42")])Remove a registered appender by name. No-op if absent.
Set the minimum log level. Messages below this level are discarded.
Override the log level for a specific module. Messages logged via log_in(module, …) consult this map first; messages logged via the bare info/debug/etc. use the global level (set with set_level).
Logger.set_module_level("MyApp.NoisyWorker", Logger.Debug)Log a Warn-level message.
v1 helper: add a (key, String) pair to the log context. Equivalent to with_field(key, LStr(value)). Retained for backward compatibility with existing callers.
Push a typed field onto the log context. Subsequent log entries carry it until either clear_context is called, or the enclosing with_scope exits.
Logger.with_field("user_id", Logger.i(42))
Logger.info("login")
-- emits: [INFO] login {user_id:42}Push a list of typed fields onto the log context in one call. Order is preserved: the first element of fields is at the BOTTOM of the pushed batch (formatters render fields in stack order).
Run thunk with fields pushed onto the log context, and pop them back off when thunk returns OR panics. Use this to attach request-scoped context (request_id, span_id, …) without having to remember to clean it up.
Logger.with_scope([Logger.LogField("request_id", Logger.s("abc123"))],
fn _ -> handle_request(req))Run thunk inside a tracing span: pushes a fresh span_id, promotes the current span (if any) to parent_span_id, and either inherits the current trace_id or starts a new one. Pops all three fields when thunk returns OR panics.
Adds two extra fields for instrumentation:
- `span_name` : LStr(name)
- `span_start_ms` : LInt(now_ms())
Logger.with_span("handle_request", fn _ -> do
Logger.info("got request")
process()
end)Push trace_id, span_id, and (optionally) parent_span_id onto the log context. Subsequent log entries carry these fields until they are popped or clear_context is called.
Logger.with_trace_context("trace-abc", "span-xyz", Some("parent-uvw"))