std/log
Leveled, structured logging to a redirectable sink.
from "std/log" import { Log, LogFormat, LogLevel, Logger }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=503Levels
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.
| Function | Meaning |
|---|---|
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.
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:
{"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
| Function | Effect |
|---|---|
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
pub struct LogThe unnamed process logger: configuration plus the four level methods and the field entry points that start a record.
Log.bool
fn Log.bool(key: &string, val: bool): LogFieldsStart an unnamed record with a boolean field.
Log.debug
fn Log.debug(msg: &string): voidLog a field-less debug record.
Log.error
fn Log.error(msg: &string): voidLog a field-less error record.
Log.float
fn Log.float(key: &string, val: f64): LogFieldsStart an unnamed record with a floating-point field.
Log.format
fn Log.format(): LogFormatThe current record format.
Log.info
fn Log.info(msg: &string): voidLog a field-less info record.
Log.int
fn Log.int(key: &string, val: i64): LogFieldsStart an unnamed record with an integer field.
Log.isEnabled
fn Log.isEnabled(level: LogLevel): boolTrue 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
fn Log.level(): LogLevelThe current threshold.
Log.setFormat
fn Log.setFormat(format: LogFormat): voidChoose how records render. Defaults to LogFormat.Text.
Log.setLevel
fn Log.setLevel(level: LogLevel): voidDiscard records below level. Defaults to Info; LogLevel.Off silences the logger entirely.
Log.setSinkFd
fn Log.setSinkFd(fd: OwnedFd): voidSend 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
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
fn Log.setSinkStderr(): voidSend records to stderr (the default).
Log.setSinkStdout
fn Log.setSinkStdout(): voidSend records to stdout.
Log.setTimestamps
fn Log.setTimestamps(enabled: bool): voidPrepend 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
fn Log.str(key: &string, val: &string): LogFieldsStart an unnamed record with a text field, e.g. Log.str("path", p).int("bytes", n).info("uploaded").
Log.warn
fn Log.warn(msg: &string): voidLog a field-less warn record.
LogFields
pub struct LogFieldsA 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
fn LogFields.bool(self: LogFields, key: &string, val: bool): LogFieldsAppend a boolean field, rendered as true/false unquoted.
LogFields.debug
fn LogFields.debug(self: LogFields, msg: &string): voidEmit the record at debug level, consuming it.
LogFields.error
fn LogFields.error(self: LogFields, msg: &string): voidEmit the record at error level, consuming it.
LogFields.float
fn LogFields.float(self: LogFields, key: &string, val: f64): LogFieldsAppend 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
fn LogFields.info(self: LogFields, msg: &string): voidEmit the record at info level, consuming it.
LogFields.int
fn LogFields.int(self: LogFields, key: &string, val: i64): LogFieldsAppend an integer field, rendered unquoted in both formats.
LogFields.str
fn LogFields.str(self: LogFields, key: &string, val: &string): LogFieldsAppend a text field. The value is copied, and quoted on output whenever bare text would be ambiguous.
LogFields.warn
fn LogFields.warn(self: LogFields, msg: &string): voidEmit the record at warn level, consuming it.
LogFormat
pub enum LogFormat: i32How a record is rendered. Text is logfmt-style for humans, Json is one JSON object per line for log shippers.
Variants:
TextJson
Logger
pub struct LoggerA 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
fn Logger.bool(self: &Logger, key: &string, val: bool): LogFieldsStart a named record with a boolean field.
Logger.debug
fn Logger.debug(self: &Logger, msg: &string): voidLog a field-less debug record under this logger's name.
Logger.error
fn Logger.error(self: &Logger, msg: &string): voidLog a field-less error record under this logger's name.
Logger.float
fn Logger.float(self: &Logger, key: &string, val: f64): LogFieldsStart a named record with a floating-point field.
Logger.info
fn Logger.info(self: &Logger, msg: &string): voidLog a field-less info record under this logger's name.
Logger.int
fn Logger.int(self: &Logger, key: &string, val: i64): LogFieldsStart a named record with an integer field.
Logger.new
fn Logger.new(name: &string): LoggerCreate a logger tagging every record with name. Cheap enough to keep one per subsystem in a global or a struct field.
Logger.str
fn Logger.str(self: &Logger, key: &string, val: &string): LogFieldsStart a named record with a text field.
Logger.warn
fn Logger.warn(self: &Logger, msg: &string): voidLog a field-less warn record under this logger's name.
LogLevel
pub enum LogLevel: i32Severity, ordered least to most urgent. Off is not a record level — it is only meaningful as a threshold, where it discards everything.
Variants:
DebugInfoWarnErrorOff