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):
# 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 passcheckbut are not run against live destinations.syslogis a no-op on Windows. async_writer(true)is presently a forward-compatible no-op (writes remain synchronous).- All field values are
string—LogLine::kv,with, and thelogvextras takestringonly. 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
colorflag, and only applies toLOG_FMT_TEXT.LogSink::stdout(true)/stderr(true)further gate color on the fd actually being a TTY, so redirected output stays clean. rotating_filerolls when the active file reachesmax_sizebytes, shiftingbase → .1 → … → .Nand keepingmax_filesrolled files; it tracks size from the file's existing length at construction.
See also
stdlib::time—now_ns,time_now_ns,time_since_ns, and theTimetype back the timestamps and@loggeddurations.- The book's HTTP chapters use this logger for request logging; HTTP itself is documented there, not here.