March Docs

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

typeLevelLevel = Debug | Info | Warn | Error#
typeLogValueLogValue =#
typeLogFieldLogField = LogField(String, LogValue)#
typeLogEntryLogEntry = LogEntry(Level, String, Int, String, List(LogField))#
typeAppenderAppender = Appender(String, LogEntry -> Unit)#

Functions

fnaa(v : Atom) : LogValue do LAtom(v) end#
fnadd_appenderadd_appender(a : Appender) : Unit#

Register an appender. Replaces any prior appender with the same name.

fnappender_callbackappender_callback(name : String, on_entry : LogEntry -> Unit) : Appender#

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.

fnappender_fileappender_file(path : String, format_fn : LogEntry -> String) : Appender#

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.

fnappender_stderrappender_stderr(format_fn : LogEntry -> String) : Appender#

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))
fnappender_stdoutappender_stdout(format_fn : LogEntry -> String) : Appender#

An appender that writes formatted entries to stdout.

fnbb(v : Bool) : LogValue do LBool(v) end#
fnclear_appendersclear_appenders() : Unit#

Remove every registered appender. Future logs will fall back to the v1 stderr text format.

fnclear_contextclear_context()#

Remove all log fields (v1 + v2).

fnclear_module_levelclear_module_level(module : String) : Unit#

Remove a module-level override; the module reverts to the global level.

fncurrent_fieldscurrent_fields() : List(LogField)#

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.

fncurrent_parent_span_idcurrent_parent_span_id() : Option(String)#

Return the parent_span_id from the log context, if set.

fncurrent_span_idcurrent_span_id() : Option(String)#

Return the current span_id from the log context, if set.

fncurrent_trace_idcurrent_trace_id() : Option(String)#

Return the current trace_id from the log context, if set.

fndebugdebug(msg) do do_log(Debug, msg, Nil) end#

Log a Debug-level message.

fnentry_fieldsentry_fields(e) do match e do LogEntry(_, _, _, _, fs) -> fs end end#
fnentry_levelentry_level(e) do match e do LogEntry(l, _, _, _, _) -> l end end#
fnentry_messageentry_message(e) do match e do LogEntry(_, m, _, _, _) -> m end end#
fnentry_sourceentry_source(e) do match e do LogEntry(_, _, _, s, _) -> s end end#
fnentry_ts_msentry_ts_ms(e) do match e do LogEntry(_, _, t, _, _) -> t end end#
fnerrorerror(msg) do do_log(Error, msg, Nil) end#

Log an Error-level message.

fnff(v : Float) : LogValue do LFloat(v) end#
fnfield_countfield_count() : Int#

Return the current depth of the field stack. Useful for tests.

fnformat_entryformat_entry(level, msg)#

Format a log entry as a simple string: [LEVEL] message

fnformat_jsonformat_json(e : LogEntry) : String#

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.
fnformat_logfmtformat_logfmt(e : LogEntry) : String#

logfmt-style formatter (Heroku / Grafana convention): level=info ts=1681400000000 msg="hello world" k=v String values containing spaces or quotes are escaped.

fnformat_textformat_text(e : LogEntry) : String#

Default text formatter. Mirrors the v1 output: [LEVEL] message {key:value, key2:value2}

fnget_levelget_level()#

Get the current minimum log level.

fnii(v : Int) : LogValue do LInt(v) end#
fninfoinfo(msg) do do_log(Info, msg, Nil) end#

Log an Info-level message.

fnint_to_levelint_to_level(n)#
fnlevel_enabledlevel_enabled(level, min_level)#

Return true if level is at least as severe as min_level.

fnlevel_forlevel_for(module : String) : Level#

Return the effective level for module — the override if one is set, otherwise the global default.

fnlevel_from_stringlevel_from_string(s)#

Parse a log level from a string (case-insensitive). Returns None for unknown strings.

fnlevel_ranklevel_rank(l)#

Return the numeric rank of a log level: Debug=0, Info=1, Warn=2, Error=3.

fnlevel_to_intlevel_to_int(l)#
fnlevel_to_stringlevel_to_string(l)#
fnlist_appenderslist_appenders() : List(String)#

Return the names of all currently registered appenders.

fnloglog(level, msg) do do_log(level, msg, Nil) end#

Log a message at the given level.

fnlog_iflog_if(level, min_level, msg)#

Log at level only if level is enabled relative to min_level.

fnlog_inlog_in(module : String, level : Level, msg : String,#

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))])
fnlog_withlog_with(level, msg, metadata)#

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")])
fnnulnul() : LogValue do LNull end#
fnremove_appenderremove_appender(name : String) : Unit#

Remove a registered appender by name. No-op if absent.

fnss(v : String) : LogValue do LStr(v) end#
fnset_levelset_level(level)#

Set the minimum log level. Messages below this level are discarded.

fnset_module_levelset_module_level(module : String, l : Level) : Unit#

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)
fnwarnwarn(msg) do do_log(Warn, msg, Nil) end#

Log a Warn-level message.

fnwith_contextwith_context(key, value)#

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.

fnwith_fieldwith_field(key : String, value : LogValue) : Unit#

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}
fnwith_fieldswith_fields(fields : List(LogField)) : Unit#

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

fnwith_scopewith_scope(fields : List(LogField), thunk : () -> a) : a#

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))
fnwith_spanwith_span(name : String, thunk : () -> a) : a#

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)
fnwith_trace_contextwith_trace_context(trace_id : String, span_id : String,#

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