Chapter 25 18 min read

Logging

stdlib::log is a structured, leveled logger. Six levels (trace < debug < info < warn < error < fatal) flow through a global Logger to one or more sinks (stdout/stderr, files, rotating files, syslog, or a callback), rendered as plain text, logfmt key=value, or one-JSON-object-per-line. Each level has a println!-style macro (info!, warn!, …) plus a bare free function (info, warn, …) routed through the process-wide global logger.

Import

import stdlib::log::*;

This single glob brings in both the runtime functions/types and the proc-macros (info!, warn!, @trace, @logged). The macro module (stdlib::log_macros) is re-imported transparently — you never import it directly.

Public surface at a glance

Group Items
Level constants LOG_TRACE LOG_DEBUG LOG_INFO LOG_WARN LOG_ERROR LOG_FATAL LOG_OFF
Format constants LOG_FMT_TEXT LOG_FMT_KV LOG_FMT_JSON
Timestamp constants LOG_TS_NONE LOG_TS_ISO LOG_TS_UNIX LOG_TS_UNIX_MS LOG_TS_RELATIVE
Sink-kind constants SINK_STDOUT SINK_STDERR SINK_FILE SINK_ROTATING SINK_SYSLOG SINK_CALLBACK
Structs Logger LoggerBuilder LogSink LogLine LogField LogEntry
Free fns (global) trace debug info warn error fatal at log_set_level log_set_format
Timing helpers trace_now_ns trace_since_str
Logger statics default global set_global
Logger methods with logv logv_at trace debug info warn error fatal
LoggerBuilder new level format timestamps add_sink async_writer build
LogSink ctors stdout stderr file rotating_file syslog callback
LogSink methods min_level
LogLine trace debug info warn error fatal kv emit emit_to
Macros trace! debug! info! warn! error! fatal!
Attributes @trace @trace(level) @logged @logged(level)

Levels, formats, timestamps, and sink kinds

All configuration values are plain i32 constants. Lower level numbers are more verbose; a logger emits an entry only when its level number is >= logger.level.

Level constants

Constant Value Meaning
LOG_TRACE 0 Most verbose; flow markers.
LOG_DEBUG 1 Debug diagnostics.
LOG_INFO 2 Normal operational messages.
LOG_WARN 3 Recoverable problems.
LOG_ERROR 4 Errors.
LOG_FATAL 5 Fatal conditions.
LOG_OFF 99 Set as a logger level to silence everything.
pub const LOG_TRACE: i32 = 0;
pub const LOG_DEBUG: i32 = 1;
pub const LOG_INFO:  i32 = 2;
pub const LOG_WARN:  i32 = 3;
pub const LOG_ERROR: i32 = 4;
pub const LOG_FATAL: i32 = 5;
pub const LOG_OFF:   i32 = 99;

Output-format constants

Constant Value Output
LOG_FMT_TEXT 0 Human-readable, color-aware (INFO server started k=v).
LOG_FMT_KV 1 logfmt: level=INFO msg="…" k=v.
LOG_FMT_JSON 2 One JSON object per line.

The same INFO entry LogLine::info("server started").kv("port", "8080").emit() renders differently per format (timestamps omitted for clarity):

output
# LOG_FMT_TEXT
INFO server started port=8080
# LOG_FMT_KV
level=INFO msg="server started" port=8080
# LOG_FMT_JSON
{"level":"INFO","msg":"server started","port":"8080"}

In KV and JSON, values containing spaces, =, quotes, tabs, or newlines are quoted/escaped (msg="slow query", \n\\n). Text format emits the message and fields verbatim. Every field value is a `string` — there is no numeric/bool field type; format your numbers before passing them.

Timestamp constants

Constant Value Rendered as
LOG_TS_NONE 0 (no timestamp)
LOG_TS_ISO 1 2026-05-23T15:30:45Z
LOG_TS_UNIX 2 1716480000 (seconds)
LOG_TS_UNIX_MS 3 1716480000123 (millis)
LOG_TS_RELATIVE 4 0.001s elapsed since logger start (rendered as <ms>ms)

Sink-kind constants

These tag a LogSink.kind; you normally create sinks with the LogSink::* constructors rather than setting kind directly.

Constant Value Sink
SINK_STDOUT 0 Standard output.
SINK_STDERR 1 Standard error.
SINK_FILE 2 Append to a file.
SINK_ROTATING 3 Size-capped rotating file.
SINK_SYSLOG 4 System log (no-op on Windows).
SINK_CALLBACK 5 User-supplied fn(string).

Quick start: the global logger

The fastest way to log. The free functions and !-macros all route through Logger::global() — a lazily-initialized singleton that defaults to INFO+ on stdout in text format.

!-macros (println!-style)

info!(fmt, args…) formats exactly like format!/println! and logs the result at the matching level. The macro expands to stdlib::log::info(format!(fmt, args…)), so a full module path is used and resolves regardless of how you imported. With no format string, one {} is synthesized per argument.

Macro Level
trace!(fmt, …) TRACE
debug!(fmt, …) DEBUG
info!(fmt, …) INFO
warn!(fmt, …) WARN
error!(fmt, …) ERROR
fatal!(fmt, …) FATAL
import stdlib::log::*;

fn main() -> i32 {
    log_set_level(LOG_DEBUG);
    log_set_format(LOG_FMT_TEXT);

    info!("server started");
    warn!("queue at {}% capacity", 90);
    error!("request failed: {}", "timeout");
    debug!("cache size {}", 1024);
    trace!("entering loop");
    return 0;
}

Free functions

Bare functions route through the global logger with no formatting. Under a qualified import they read as log::info(...).

Function Signature Logs at
trace pub fn trace(msg: string) TRACE
debug pub fn debug(msg: string) DEBUG
info pub fn info(msg: string) INFO
warn pub fn warn(msg: string) WARN
error pub fn error(msg: string) ERROR
fatal pub fn fatal(msg: string) FATAL
at pub fn at(lvl: i32, msg: string) lvl
import stdlib::log::*;

fn main() -> i32 {
    info("bare fn info");
    error("bare fn error");
    at(LOG_INFO, "ready");        // log at an explicit level
    return 0;
}

Global configuration

Function Signature Effect
log_set_level pub fn log_set_level(lvl: i32) Set the global logger's minimum level.
log_set_format pub fn log_set_format(fmt: i32) Set the global logger's output format.

These are conveniences over Logger::global().level = lvl / .format = fmt.

Logger

The logger holds a level, format, timestamp style, a start time (for relative timestamps), its sinks, and any bound fields inherited by child loggers.

pub struct Logger {
    pub level: i32,
    pub format: i32,
    pub ts_format: i32,
    pub time_start_ns: i64,
    pub sinks: *Vector<*LogSink>,
    pub bound_fields: *Vector<LogField>,
}

Constructors and the global singleton

Function Signature Description
default pub fn default() -> *Logger INFO+ to stdout, color when stdout is a TTY, ISO timestamps.
global pub fn global() -> *Logger The process-wide singleton (lazily created on first use).
set_global pub fn set_global(l: *Logger) Replace the global logger.

Logger::global() is created on first use by calling Logger::default(). The free functions (info, warn, …), the !-macros, at, log_set_level, and log_set_format all route through it. To make a custom logger the default for those entry points, build it and pass it to Logger::set_global.

import stdlib::log::*;

fn main() -> i32 {
    let l: *Logger = Logger::default();   // INFO+, stdout, ISO ts, color on TTY
    l.info("from default logger");

    Logger::set_global(l);                // install as the process-wide logger
    log_set_level(LOG_DEBUG);             // tweaks the now-global logger
    log_set_format(LOG_FMT_KV);

    Logger::global().debug("now visible at DEBUG");
    info("global free fn");               // routes through the same logger
    return 0;
}

Direct-call methods

Each method emits a message at its fixed level. They do not capture file:line.

pub fn trace(self: *Logger, msg: string)
pub fn debug(self: *Logger, msg: string)
pub fn info (self: *Logger, msg: string)
pub fn warn (self: *Logger, msg: string)
pub fn error(self: *Logger, msg: string)
pub fn fatal(self: *Logger, msg: string)

Child loggers — with

with returns a child logger that inherits sinks/level/format/timestamps and adds one bound field. Every entry from the child carries that field. Chain with to bind several.

pub fn with(self: *Logger, key: string, value: string) -> *Logger

Low-level emit — logv / logv_at

extras is a flat list of key, value, key, value, …. logv_at is the same but with caller-supplied source location, which the rendered output includes as (file:line).

pub fn logv(self: *Logger, lvl: i32, msg: string, extras: *Vector<string>)
pub fn logv_at(self: *Logger, lvl: i32, msg: string, file: string, line: i32,
               extras: *Vector<string>)
import stdlib::log::*;

fn main() -> i32 {
    let l: *Logger = LoggerBuilder::new()
        .level(LOG_DEBUG)
        .add_sink(LogSink::stdout(true))
        .add_sink(LogSink::file("/var/log/app.log"))
        .format(LOG_FMT_JSON)
        .timestamps(LOG_TS_ISO)
        .build();

    l.info("started");

    // Child logger: req_id + user attached to every line.
    let req_log: *Logger = l.with("req_id", "abc123").with("user", "alice");
    req_log.info("handled");

    // Low-level: flat key/value extras + explicit source location.
    let extras: *Vector<string> = Vector::new();
    extras.push("port");
    extras.push("8080");
    l.logv(LOG_INFO, "with extras", extras);
    l.logv_at(LOG_INFO, "with loc", "main.glide", 42, extras);

    Logger::set_global(l);              // make it the global
    Logger::global().info("via global");
    return 0;
}

In KV format the same logv / logv_at calls render with the extras as trailing key=value pairs and the location as file=… line=…:

import stdlib::log::*;

fn main() -> i32 {
    let l: *Logger = LoggerBuilder::new().format(LOG_FMT_KV).build();

    let extras: *Vector<string> = Vector::new();
    extras.push("port");  extras.push("8080");
    extras.push("tls");   extras.push("on");
    l.logv(LOG_INFO, "listening", extras);
    // level=INFO msg=listening port=8080 tls=on

    l.logv_at(LOG_ERROR, "boom", "server.glide", 128, extras);
    // level=ERROR msg=boom file=server.glide line=128 port=8080 tls=on
    return 0;
}

LoggerBuilder

Fluent builder for a custom Logger. Every setter returns self for chaining; build() produces the *Logger. If no sink is added, build() defaults to a colorized stdout sink.

Method Signature Description
new pub fn new() -> *LoggerBuilder Start with defaults (INFO, text, ISO timestamps, no sinks).
level pub fn level(self: *LoggerBuilder, lvl: i32) -> *LoggerBuilder Minimum level.
format pub fn format(self: *LoggerBuilder, fmt: i32) -> *LoggerBuilder Output format.
timestamps pub fn timestamps(self: *LoggerBuilder, ts: i32) -> *LoggerBuilder Timestamp style.
add_sink pub fn add_sink(self: *LoggerBuilder, s: *LogSink) -> *LoggerBuilder Append a sink (call repeatedly).
async_writer pub fn async_writer(self: *LoggerBuilder, on: bool) -> *LoggerBuilder Request non-blocking writes.
build pub fn build(self: *LoggerBuilder) -> *Logger Finalize.

Choosing format and timestamps

Each builder produces an independent logger — handy for routing different subsystems to different formats. The timestamp style is applied uniformly to whatever format is selected.

import stdlib::log::*;

fn main() -> i32 {
    let kv: *Logger = LoggerBuilder::new()
        .format(LOG_FMT_KV)
        .timestamps(LOG_TS_UNIX)
        .add_sink(LogSink::stdout(false))
        .build();
    LogLine::warn("slow query").kv("ms", "320").kv("table", "users").emit_to(kv);

    let js: *Logger = LoggerBuilder::new()
        .format(LOG_FMT_JSON)
        .timestamps(LOG_TS_ISO)
        .build();
    js.with("svc", "api").info("ready");

    let rel: *Logger = LoggerBuilder::new()
        .timestamps(LOG_TS_RELATIVE)        // <ms>ms since logger start
        .format(LOG_FMT_TEXT)
        .build();
    rel.info("relative timestamp");

    let none: *Logger = LoggerBuilder::new().timestamps(LOG_TS_NONE).build();
    none.info("no timestamp");
    return 0;
}

LogSink

A sink is one output destination with its own minimum level. Construct sinks with the static constructors, optionally narrow them with min_level, and attach them via LoggerBuilder::add_sink.

pub struct LogSink {
    pub kind: i32,
    pub level: i32,           // per-sink filter (LOG_TRACE by default)
    pub color: bool,          // ANSI for text-format
    pub fd: i32,              // raw fd for stdout/stderr/file
    pub path: string,
    pub max_size: i32,        // rotating: bytes per file
    pub max_files: i32,       // rotating: keep N rolled files
    pub cur_size: i32,        // rotating: current size in active file
    pub cb: fn(string),       // SINK_CALLBACK
}

Constructors

Constructor Signature Description
stdout pub fn stdout(color: bool) -> *LogSink stdout; color only applied when fd 1 is a TTY.
stderr pub fn stderr(color: bool) -> *LogSink stderr; color only applied when fd 2 is a TTY.
file pub fn file(path: string) -> *LogSink Append to path (creates it).
rotating_file pub fn rotating_file(path: string, max_size: i32, max_files: i32) -> *LogSink Roll to .1.N when max_size bytes is hit, keeping max_files rolled files.
syslog pub fn syslog(ident: string) -> *LogSink POSIX syslog with the given ident (no-op on Windows).
callback pub fn callback(f: fn(string)) -> *LogSink Invoke f with each fully-rendered line.

Methods

Method Signature Description
min_level pub fn min_level(self: *LogSink, lvl: i32) -> *LogSink Reject entries below lvl at this sink. Chainable.
import stdlib::log::*;

fn audit(line: string) {
    // forward each rendered line somewhere (e.g. an audit trail)
    return;
}

fn main() -> i32 {
    let l: *Logger = LoggerBuilder::new()
        .add_sink(LogSink::stdout(false).min_level(LOG_WARN))   // warnings+ on stdout
        .add_sink(LogSink::file("/var/log/app.log"))            // everything to file
        .add_sink(LogSink::rotating_file("/var/log/app.log", 10485760, 5))
        .add_sink(LogSink::syslog("myapp"))
        .add_sink(LogSink::callback(audit))
        .build();
    l.info("multi-sink");
    return 0;
}

An entry must clear two gates: the logger's level (decides whether the entry is built at all) and then each sink's own level (decides whether that sink writes it). A sink defaults to LOG_TRACE, so by default it accepts everything the logger emits.

import stdlib::log::*;

fn main() -> i32 {
    let l: *Logger = LoggerBuilder::new()
        .level(LOG_DEBUG)
        .add_sink(LogSink::stdout(false).min_level(LOG_WARN))
        .add_sink(LogSink::file("/tmp/app.log"))   // inherits LOG_TRACE floor
        .build();

    l.debug("only in file");      // below console floor -> file only
    l.warn("file + console");     // passes both sink floors
    return 0;
}

Callback sinks and LogField

A SINK_CALLBACK sink hands your fn(string) each fully-rendered line (already formatted and newline-terminated). Use it to bridge into an audit trail, a ring buffer, a metrics counter, or a test harness. The LogField struct is the public {key, value} carrier the renderers iterate; both key and value are string.

import stdlib::log::*;

fn render(line: string) {
    // Forward the rendered line somewhere: an audit trail, a ring
    // buffer, a metrics counter, etc. Here we just inspect its length.
    let _n: i32 = line.len();
    return;
}

fn main() -> i32 {
    let f: LogField = LogField { key: "user", value: "alice" };
    println!("field {}={}", f.key, f.value);

    let l: *Logger = LoggerBuilder::new()
        .format(LOG_FMT_KV)
        .timestamps(LOG_TS_NONE)
        .add_sink(LogSink::callback(render))
        .build();
    LogLine::info("login").kv("user", f.value).emit_to(l);
    return 0;
}

LogLine — fluent structured fields

When you want key=value fields on a single line without binding them on a child logger, build a LogLine, chain kv, then emit (global) or emit_to (a specific logger).

Method Signature Description
trace pub fn trace(msg: string) -> *LogLine Start a TRACE line.
debug pub fn debug(msg: string) -> *LogLine Start a DEBUG line.
info pub fn info(msg: string) -> *LogLine Start an INFO line.
warn pub fn warn(msg: string) -> *LogLine Start a WARN line.
error pub fn error(msg: string) -> *LogLine Start an ERROR line.
fatal pub fn fatal(msg: string) -> *LogLine Start a FATAL line.
kv pub fn kv(self: *LogLine, k: string, v: string) -> *LogLine Attach a field. Chainable.
emit pub fn emit(self: *LogLine) Send through the global logger.
emit_to pub fn emit_to(self: *LogLine, lg: *Logger) Send through lg.
import stdlib::log::*;

fn main() -> i32 {
    LogLine::info("server started")
        .kv("port", "8080")
        .kv("workers", "4")
        .emit();

    let l: *Logger = LoggerBuilder::new().build();
    LogLine::warn("slow query").kv("ms", "320").emit_to(l);
    return 0;
}

LogField and LogEntry

The data carried by a single log record. You rarely construct these directly — logv/LogLine/with build them for you — but the fields are public for renderers and callbacks.

pub struct LogField {
    pub key: string,
    pub value: string,
}

pub struct LogEntry {
    pub level: i32,
    pub msg: string,
    pub time_ns: i64,
    pub file: string,
    pub line: i32,
    pub fields: *Vector<LogField>,
}

Flow-tracing attributes — @trace and @logged

These proc-attributes wrap a function so it logs on entry and exit. The optional first argument picks the level (trace, debug, info, warn, error, fatal); the default is debug.

Attribute Logs
@trace > name on entry, < name on exit (bare flow markers).
@trace(info) Same, but at the named level.
@logged Entry with argument values (> name(4, 6)), exit with the return value and elapsed time (< name -> 10 (340ns)).
@logged(info) Same, at the named level.

A no-return function omits the -> {} part on exit (< name (340ns)). The level identifier is the bare word info, not LOG_INFO. Both attributes route their entry/exit lines through the global logger via the matching free function, so the lines obey the global level.

import stdlib::log::*;

@trace
fn step() { return; }                          // logs `> step` / `< step`

@trace(info)
fn ping() -> i32 { return 1; }                 // entry/exit at INFO

@logged
fn add(a: i32, b: i32) -> i32 { return a + b; } // logs args, return, duration

fn main() -> i32 {
    log_set_level(LOG_TRACE);
    step();
    let p: i32 = ping();
    let s: i32 = add(4, 6);

    let t0: i64 = trace_now_ns();
    let dur: string = trace_since_str(t0);
    info!("p={} s={} dur={}", p, s, dur);
    return 0;
}

Timing helpers

@logged is backed by two public helpers you can also call directly to measure a span.

Function Signature Description
trace_now_ns pub fn trace_now_ns() -> i64 Monotonic nanosecond mark.
trace_since_str pub fn trace_since_str(start_ns: i64) -> string Elapsed since a mark, auto-scaled (340ns / 12.3us / 4.5ms / 1.2s).
import stdlib::log::*;

fn main() -> i32 {
    let t0: i64 = trace_now_ns();
    // ... work ...
    let elapsed: string = trace_since_str(t0);
    info!("done in {}", elapsed);
    return 0;
}

Caveats

  • Sinks that touch real resources (LogSink::file, rotating_file, syslog) open files / connect to syslog at construction time; the examples above construct them and pass check but are not run against live destinations. syslog is a no-op on Windows.
  • async_writer(true) is presently a forward-compatible no-op (writes remain synchronous).
  • All field values are stringLogLine::kv, with, and the logv extras take string only. Format numbers/bools (format!("{}", n)) before passing them.
  • To silence a logger entirely set its level to LOG_OFF (99); no level number reaches it, so nothing is emitted regardless of per-sink filters.
  • Color is decided once, from the first sink's color flag, and only applies to LOG_FMT_TEXT. LogSink::stdout(true) / stderr(true) further gate color on the fd actually being a TTY, so redirected output stays clean.
  • rotating_file rolls when the active file reaches max_size bytes, shifting base → .1 → … → .N and keeping max_files rolled files; it tracks size from the file's existing length at construction.

See also

  • stdlib::timenow_ns, time_now_ns, time_since_ns, and the Time type back the timestamps and @logged durations.
  • The book's HTTP chapters use this logger for request logging; HTTP itself is documented there, not here.