core:log
Structured logging and tracing (docs/wep-2026-06-25-core-log.md).
A subscriber is an effect: sinks and layers are handlers of Log, so
composition is handler nesting and dispatch stays static. Events carry a
level, a message, structured fields, and the caller's source location;
spans are first-class values entered for a scope.
Filtering is three gates, cheapest first, each narrowing what the one
before admitted: -D log.level=info folds at compile time, set_log_level
is one global read, and Log::enabled runs the installed layer stack.
enabled(level) exposes the same ladder for guarding fields too expensive
to build.
The facade is #[ambient], so performing Log adds no with Log to a
caller's signature, and every operation carries a default implementation, so
a program that installs nothing still logs — Info and above, one text line
per event on stderr. Installing a sink replaces that for its scope; NopSink
is how a program asks for silence.
Synopsis
// No sink installed: the default subscriber writes it to stderr.
info(`starting up`);
// An installed sink takes over for its scope. `CaptureSink` is the one a
// test reads back; `TextSink` and `JsonSink` write. The event asserted on is
// at `Error`, the level no build threshold folds away, so what this reads
// back does not depend on how the build was configured.
let mut sink = CaptureSink {};
with Log => &mut sink do {
in_span(&span(Level::Info, `request`), || {
error(`disk full`, { free_bytes: 0 });
});
};
assert sink.events.len() == 1;
assert sink.events[0].message == "disk full";
assert sink.events[0].meta.level matches { Error };
assert sink.events[0].parent matches { Some(1) };
Functions
pub fn level_from_str(s: String) -> Level
Infallible LenientFromStr wrapper: an unrecognized name is Trace, so a
typo widens what is logged rather than silencing a program.
pub fn set_log_level(level: Level)
Set the process-wide threshold (tier 2). A sink or layer narrows further.
pub fn log_level() -> Level
The process-wide threshold set by [set_log_level].
pub fn enabled(level: Level) -> bool
#[ambient]
The three tiers, cheapest first, each narrowing what the one before admitted. Exposed as a predicate too, for guarding fields too expensive to build:
if enabled(Level::Debug) {
debug(`state`, { snapshot: expensive_snapshot() });
}
pub fn event<T: Serialize = NoFields>(level: Level, message: String, fields: T = NoFields {}, target: String = #function, file: String = #file, line: i32 = #line)
#[ambient]
Emit an event at a level a caller computes. The level wrappers' three gates apply here too; only tier 1 comes out weaker, since a level that is not a literal is not a comparison the optimizer can fold. Location defaults resolve at the call site.
pub fn trace<T: Serialize = NoFields>(message: String, fields: T = NoFields {}, target: String = #function, file: String = #file, line: i32 = #line)
#[ambient]
pub fn debug<T: Serialize = NoFields>(message: String, fields: T = NoFields {}, target: String = #function, file: String = #file, line: i32 = #line)
#[ambient]
pub fn info<T: Serialize = NoFields>(message: String, fields: T = NoFields {}, target: String = #function, file: String = #file, line: i32 = #line)
#[ambient]
pub fn warn<T: Serialize = NoFields>(message: String, fields: T = NoFields {}, target: String = #function, file: String = #file, line: i32 = #line)
#[ambient]
pub fn error<T: Serialize = NoFields>(message: String, fields: T = NoFields {}, target: String = #function, file: String = #file, line: i32 = #line)
#[ambient]
pub fn span<T: Serialize = NoFields>(level: Level, name: String, fields: T = NoFields {}, target: String = #function, file: String = #file, line: i32 = #line) -> Span
#[ambient]
pub fn current() -> Option<Span>
#[ambient]
The innermost entered span, or null outside any.
pub fn in_span<T, effect E>(s: &Span, mut body: fn mut() -> T with E) -> T with E
#[ambient]
Run body inside s. A closure can't skip the exit/close calls, so they
run on every body exit path, an early return included.
pub fn parse_directives(spec: String) -> List<Directive>
Parse an EnvFilter-style comma-separated list — info,Db::query=debug,handle=trace
— from WADO_LOG or any string an application supplies. A bare level is the
default for unmatched targets; target=level overrides it for targets under
that prefix. A malformed directive is skipped: a typo in an environment
variable must not stop a program from starting.
Traits
pub trait Clock with ()
How a sink stamps an event with a time, or declines to. None is no stamp.
A type parameter rather than a field, because only a type can remove the
clock: a sink over [NoClock] monomorphizes to code that never reads one, so
the component carries no wasi:clocks import. A runtime flag leaves the call
reachable, and at -O0 nothing folds it away.
fn now() -> Option<String>
Effects
pub interface Log
The subscriber. Sinks and layers are handlers of it, so composition is handler nesting and dispatch stays static. Operations are best-effort: a handler swallows its own write and serialize errors and never fails the program.
Every operation carries a default implementation, so a program that installs
no sink logs to stderr rather than trapping — logging is a debugging aid, and
one that crashes the program for want of setup is worse than no logging. The
defaults are a TextSink on stderr admitting Info and above; installing
any handler replaces them for its scope, and NopSink is how a program asks
for silence.
fn enabled(level: Level) -> bool
fn current_span() -> Option<SpanId>
fn new_span(attrs: SpanAttrs) -> SpanId
fn record_fields(span: SpanId, fields: Value)
fn follows_from(span: SpanId, cause: SpanId)
fn enter(span: SpanId)
fn exit(span: SpanId)
fn close(span: SpanId)
fn event(event: Event)
Types
pub type SpanId = u64
Structs
pub struct NoFields
Serializes to {}; the default field payload, so info(msg) needs no
second argument and the default type parameter supplies its type.
pub struct Metadata
What a callsite says about itself, independent of the payload.
level: Level
target: String
Category; defaults to the caller's #function.
name: String
Span name; "" for events.
file: String
line: i32
pub struct Event
meta: Metadata
message: String
fields: Value
parent: Option<SpanId>
pub struct SpanAttrs
meta: Metadata
fields: Value
pub struct Span
A span is a first-class value, so a caller can carry one and re-enter it.
Fields are private.
pub fn id(&self) -> SpanId
pub fn record<T: Serialize>(&self, fields: T)
#[ambient]
Add fields to an open span.
pub fn follows_from(&self, cause: &Span)
#[ambient]
Record a non-parent causal link: this span follows from cause.
pub fn close(&self)
#[ambient]
Close a span entered by hand. [in_span] closes on every exit path, so
this is only for a span the caller opened without it.
pub struct NoClock
No timestamp, and no wasi:clocks import. The default sink's clock, so a
program that installs nothing logs in any world, core:kiln/generator
included — its linker offers no WASI at all.
This is the conservative end of the trade, not a verdict on what a default line should carry. Stamping the worlds that do have a clock is a compatible change, and may replace this once a mechanism keeps the guarantee.
impl Clock for NoClock
fn now() -> Option<String>
pub struct WallClock
Wall-clock stamps with millisecond precision. Installing a sink over this
adds a wasi:clocks import, so it belongs only in a world that has one.
#[ambient] for the same reason the facade is: a sink's operations are
Log's, whose signatures carry no effect, so the clock read cannot be
declared on them.
impl Clock for WallClock
fn now() -> Option<String>
#[ambient]
pub struct TextSink<C: Clock = NoClock>
One human-readable line per event on stderr:
2026-08-08T11:47:08.204Z INFO 42 handle_request: user logged in user_id=7 ip=127.0.0.1
Each part its configuration turns off is omitted; location appends
at file:line. Writes through the ambient log_stderr, so the sink needs
no Stderr in its signature and installs in any world. C selects the
timestamp: TextSink<WallClock> stamps, the default TextSink does not.
seq: bool
location: bool
pub fn render(&mut self, event: &Event) -> String
The line this sink would write for event, without writing it — the
hook for sending the same format somewhere other than stderr. Consumes
a sequence number, so one call is one line.
impl Log for TextSink<C>
fn enabled(&self, level: Level) -> bool
fn current_span(&self) -> Option<SpanId>
fn new_span(&mut self, attrs: SpanAttrs) -> SpanId
fn record_fields(&mut self, span: SpanId, fields: Value)
fn follows_from(&mut self, span: SpanId, cause: SpanId)
fn enter(&mut self, span: SpanId)
fn exit(&mut self, span: SpanId)
fn close(&mut self, span: SpanId)
fn event(&mut self, event: Event)
pub struct JsonSink<C: Clock = NoClock>
One JSON object per line (JSONL) on stderr, through json::to_string:
{"seq":42,"level":"info","target":"handle_request","message":"user logged in","span":3,"fields":{"user_id":7}}
Keys a switch turns off are absent rather than null. C selects the ts
key the way it does for [TextSink], so the default over NoClock writes
no ts and JsonSink<WallClock> leads with one.
seq: bool
location: bool
pub fn render(&mut self, event: &Event) -> String
The JSON line this sink would write for event, without writing it.
Consumes a sequence number, so one call is one line. A sink swallows its
own serialize errors, so an unserializable event renders as {} rather
than aborting the program.
impl Log for JsonSink<C>
fn enabled(&self, level: Level) -> bool
fn current_span(&self) -> Option<SpanId>
fn new_span(&mut self, attrs: SpanAttrs) -> SpanId
fn record_fields(&mut self, span: SpanId, fields: Value)
fn follows_from(&mut self, span: SpanId, cause: SpanId)
fn enter(&mut self, span: SpanId)
fn exit(&mut self, span: SpanId)
fn close(&mut self, span: SpanId)
fn event(&mut self, event: Event)
pub struct NopSink
Drops everything, cheaply. The sink to install when a program must not log
but its libraries still perform Log.
Fields are private.
impl Log for NopSink
fn enabled(&self, level: Level) -> bool
fn current_span(&self) -> Option<SpanId>
fn new_span(&mut self, attrs: SpanAttrs) -> SpanId
fn record_fields(&mut self, span: SpanId, fields: Value)
fn follows_from(&mut self, span: SpanId, cause: SpanId)
fn enter(&mut self, span: SpanId)
fn exit(&mut self, span: SpanId)
fn close(&mut self, span: SpanId)
fn event(&mut self, event: Event)
pub struct CaptureSink
Keeps every event and span in memory, for tests: assertions read the
captured values rather than parsing output. ops records the span
lifecycle in order — new:name#id, enter:id, exit:id, close:id,
record:id:key, follows:id<-cause.
min: Level
Tier-3 threshold, so a test can exercise the sink's own gate.
events: List<Event>
spans: List<SpanAttrs>
Spans in id order: spans[i] is the span with id i + 1.
ops: List<String>
impl Log for CaptureSink
fn enabled(&self, level: Level) -> bool
fn current_span(&self) -> Option<SpanId>
fn new_span(&mut self, attrs: SpanAttrs) -> SpanId
fn record_fields(&mut self, span: SpanId, fields: Value)
fn follows_from(&mut self, span: SpanId, cause: SpanId)
fn enter(&mut self, span: SpanId)
fn exit(&mut self, span: SpanId)
fn close(&mut self, span: SpanId)
fn event(&mut self, event: Event)
pub struct Context<T: Serialize>
slog's With: prepend fixed fields to every event passing through.
A key the event itself carries wins.
fields: T
impl Log for Context<T>
fn event(&self, event: Event) with Log
pub struct Directive
One EnvFilter directive: the lowest level admitted for targets under
target. An empty target is the default for unmatched targets.
A target is matched by prefix against the event's, which defaults to the
caller's #function — a bare name for a free function (handle_request),
Type::method for a method, Log::event for an operation's default. Pass
target explicitly at the call site to group events under a name of your
own (a subsystem, say) rather than the function that emitted them.
target: String
level: Level
pub struct Filter
Per-target filtering, EnvFilter-style: the longest matching target prefix
wins, and a target no directive matches is admitted. See [Directive] for
what an event's target is.
directives: List<Directive>
pub fn threshold(&self, target: String) -> Level
The lowest level admitted for target.
pub fn floor(&self) -> Level
The lowest level any target could be admitted at. Log::enabled sees no
target, so the gate has to be the floor over every directive — a
narrower answer would drop events event would go on to admit.
impl Log for Filter
fn enabled(&self, level: Level) -> bool with Log
fn event(&self, event: Event) with Log
Enums
pub enum Level
Severity, ordered by declaration: a threshold is the lowest level emitted,
so at Info both Trace and Debug are off.
