Skip to content

std/log ​

Leveled, structured logging to a redirectable sink.

milo
from "std/log" import { Log, LogFormat, LogLevel, Logger }
milo
Log.setLevel(LogLevel.Debug)
Log.info("server starting")
Log.str("path", p).int("bytes", n).warn("upload retried")

let http = Logger.new("http")
http.int("status", 503).error("upstream refused")
2026-08-04T05:11:00.792Z INFO  server starting
2026-08-04T05:11:00.792Z WARN  upload retried path=/var/tmp/a.bin bytes=4096
2026-08-04T05:11:00.793Z ERROR [http] upstream refused status=503

Levels ​

LogLevel is ordered Debug < Info < Warn < Error < Off. Records below the threshold are discarded. The default threshold is Info, so debug records need Log.setLevel(LogLevel.Debug) to appear. LogLevel.Off is a threshold only — it silences everything and is never a record level.

FunctionMeaning
Log.setLevel(level)Set the process-wide threshold
Log.level()Read the threshold
Log.isEnabled(level)Would a record at level be emitted?

Log.debug/info/warn/error check the threshold themselves, but only after their arguments are evaluated. Guard an expensive message with Log.isEnabled first.

Fields ​

A record is a message plus ordered key/value pairs. str, int, float, and bool start a record on Log or a Logger and chain; a level method consumes it and emits.

milo
Log.str("path", path).int("bytes", n).bool("cached", hit).float("secs", t).info("served")

Text output leaves values bare unless they would be ambiguous (empty, or containing a space, quote, backslash, =, or a control byte), in which case they are quoted and escaped. Integers and booleans are always bare. A NaN or infinite float is quoted, because JSON has no literal for it.

Named loggers ​

Logger.new(name) tags every record with a subsystem name — [name] in text, "logger" in JSON. Filtering stays process-wide: a Logger is a label, not an independently configurable channel.

Formats ​

Log.setFormat(LogFormat.Json) emits one JSON object per line:

json
{"time":"2026-08-04T05:11:00.792Z","level":"WARN","logger":"http","msg":"slow","ms":1200}

Log.setTimestamps(false) drops the timestamp — for a sink that stamps lines itself (journald, a container runtime) or when output must be reproducible.

Sinks ​

FunctionEffect
Log.setSinkFd(fd)Write to an already-open descriptor. It stays yours to close, and must outlive the last record. Default is 2 (stderr)
Log.setSinkPath(path)Open path for append and own it until the sink is replaced. Returns Result<Unit, IoError>

Records go straight at the descriptor, but stdio is flushed first, so a sink on fd 1 interleaves correctly with print.

Concurrency ​

Milo has no mutex, and std/log does not serialize writers. What it guarantees instead is that a record renders to one complete line and reaches the sink through exactly one write(2). POSIX makes a single write to a pipe or terminal atomic up to PIPE_BUF (4096 bytes), and a single write to an O_APPEND file atomic at any size. Records from concurrent tasks therefore interleave whole, in some order; only a record longer than PIPE_BUF on a pipe can tear.

Configuration is process-wide mutable state meant to be set once at startup. Reconfiguring while other tasks log cannot corrupt anything — each setting is a single machine word — but concurrent readers may observe the old value for an unbounded time, and a setSinkPath that closes the previous owned descriptor can race a write already in flight. Configure before you spawn.

API reference ​

Log ​

milo
pub struct Log

The unnamed process logger: configuration plus the four level methods and the field entry points that start a record.

Log.bool ​

milo
fn Log.bool(key: &string, val: bool): LogFields

Start an unnamed record with a boolean field.

Log.debug ​

milo
fn Log.debug(msg: &string): void

Log a field-less debug record.

Log.error ​

milo
fn Log.error(msg: &string): void

Log a field-less error record.

Log.float ​

milo
fn Log.float(key: &string, val: f64): LogFields

Start an unnamed record with a floating-point field.

Log.format ​

milo
fn Log.format(): LogFormat

The current record format.

Log.info ​

milo
fn Log.info(msg: &string): void

Log a field-less info record.

Log.int ​

milo
fn Log.int(key: &string, val: i64): LogFields

Start an unnamed record with an integer field.

Log.isEnabled ​

milo
fn Log.isEnabled(level: LogLevel): bool

True when a record at level would be emitted. Check this before building an expensive message or field set; the level methods check it themselves, but only after their arguments have been evaluated.

Log.level ​

milo
fn Log.level(): LogLevel

The current threshold.

Log.setFormat ​

milo
fn Log.setFormat(format: LogFormat): void

Choose how records render. Defaults to LogFormat.Text.

Log.setLevel ​

milo
fn Log.setLevel(level: LogLevel): void

Discard records below level. Defaults to Info; LogLevel.Off silences the logger entirely.

Log.setSinkFd ​

milo
fn Log.setSinkFd(fd: OwnedFd): void

Send records to a descriptor you opened (a pipe end, a socket). The log takes it over: it is closed when the sink is replaced, so no record can be written to a number something else has since reused.

Log.setSinkPath ​

milo
fn Log.setSinkPath(path: &string): Result<Unit, IoError>

Append records to path, creating it if absent. The descriptor is opened and owned here for the life of the process (or until the sink is replaced). O_APPEND is what makes concurrent whole-record writes atomic at any size.

Log.setSinkStderr ​

milo
fn Log.setSinkStderr(): void

Send records to stderr (the default).

Log.setSinkStdout ​

milo
fn Log.setSinkStdout(): void

Send records to stdout.

Log.setTimestamps ​

milo
fn Log.setTimestamps(enabled: bool): void

Prepend an ISO-8601 UTC timestamp to every record. On by default, except when JOURNAL_STREAM is set (systemd sets it when stderr goes to the journal, which stamps lines itself); calling this overrides that either way. Turn it off for a container runtime that also stamps lines, or when output must be byte-for-byte reproducible.

Log.str ​

milo
fn Log.str(key: &string, val: &string): LogFields

Start an unnamed record with a text field, e.g. Log.str("path", p).int("bytes", n).info("uploaded").

Log.warn ​

milo
fn Log.warn(msg: &string): void

Log a field-less warn record.

LogFields ​

milo
pub struct LogFields

A record under construction: an optional logger name plus ordered key/value pairs. Built by the str/int/float/bool entry points on Log or a Logger, chained, and consumed by a level method that emits it.

Log.str("path", p).int("bytes", n).info("upload complete")

quoted records whether a value is textual (and so needs quoting in JSON and possibly in text) rather than an already-valid numeric or boolean literal.

Fields: name: string, keys: Vec<string>, vals: Vec<string>, quoted: Vec<bool>.

LogFields.bool ​

milo
fn LogFields.bool(self: LogFields, key: &string, val: bool): LogFields

Append a boolean field, rendered as true/false unquoted.

LogFields.debug ​

milo
fn LogFields.debug(self: LogFields, msg: &string): void

Emit the record at debug level, consuming it.

LogFields.error ​

milo
fn LogFields.error(self: LogFields, msg: &string): void

Emit the record at error level, consuming it.

LogFields.float ​

milo
fn LogFields.float(self: LogFields, key: &string, val: f64): LogFields

Append a floating-point field, rendered unquoted in both formats. A NaN or infinite value is quoted instead, since JSON has no literal for it.

LogFields.info ​

milo
fn LogFields.info(self: LogFields, msg: &string): void

Emit the record at info level, consuming it.

LogFields.int ​

milo
fn LogFields.int(self: LogFields, key: &string, val: i64): LogFields

Append an integer field, rendered unquoted in both formats.

LogFields.str ​

milo
fn LogFields.str(self: LogFields, key: &string, val: &string): LogFields

Append a text field. The value is copied, and quoted on output whenever bare text would be ambiguous.

LogFields.warn ​

milo
fn LogFields.warn(self: LogFields, msg: &string): void

Emit the record at warn level, consuming it.

LogFormat ​

milo
pub enum LogFormat: i32

How a record is rendered. Text is logfmt-style for humans, Json is one JSON object per line for log shippers.

Variants:

  • Text
  • Json

Logger ​

milo
pub struct Logger

A named logger. The name is a routing/grep tag rendered as [name] in text and as "logger" in JSON; filtering stays process-wide, so a Logger is a label, not an independently configurable channel.

Fields: name: string.

Logger.bool ​

milo
fn Logger.bool(self: &Logger, key: &string, val: bool): LogFields

Start a named record with a boolean field.

Logger.debug ​

milo
fn Logger.debug(self: &Logger, msg: &string): void

Log a field-less debug record under this logger's name.

Logger.error ​

milo
fn Logger.error(self: &Logger, msg: &string): void

Log a field-less error record under this logger's name.

Logger.float ​

milo
fn Logger.float(self: &Logger, key: &string, val: f64): LogFields

Start a named record with a floating-point field.

Logger.info ​

milo
fn Logger.info(self: &Logger, msg: &string): void

Log a field-less info record under this logger's name.

Logger.int ​

milo
fn Logger.int(self: &Logger, key: &string, val: i64): LogFields

Start a named record with an integer field.

Logger.new ​

milo
fn Logger.new(name: &string): Logger

Create a logger tagging every record with name. Cheap enough to keep one per subsystem in a global or a struct field.

Logger.str ​

milo
fn Logger.str(self: &Logger, key: &string, val: &string): LogFields

Start a named record with a text field.

Logger.warn ​

milo
fn Logger.warn(self: &Logger, msg: &string): void

Log a field-less warn record under this logger's name.

LogLevel ​

milo
pub enum LogLevel: i32

Severity, ordered least to most urgent. Off is not a record level — it is only meaningful as a threshold, where it discards everything.

Variants:

  • Debug
  • Info
  • Warn
  • Error
  • Off