01Level
A number, a name and a colour. Six are built in; a seventh is a value you construct.
Level::parse · ::label · ::at_leastmoonlog keeps a log entry typed until it is written: a level, a target, a message and ordered fields, filtered once, shaped by a layout, and handed to a sink that is allowed to fail.
// {time:%H:%M:%S%.3f} {level:<5} {target}: {msg}{fields} 22:13:20.412 WARN mirror::sync: disk 91% full mirror=debian free_gb=1.5
time=2023-11-14T22:13:20Z level=warn target=mirror::sync msg="disk 91% full" mirror=debian free_gb=1.5
2023-11-14 22:13:20.412 WARN mirror::sync disk 91% full mirror = debian free_gb = 1.5
{
"time": "2023-11-14T22:13:20.412Z",
"level": "warn",
"target": "mirror::sync",
"msg": "disk 91% full",
"mirror": "debian",
"free_gb": 1.5,
"app": "gateway", "env": "prod"
}
A number, a name and a colour. Six are built in; a seventh is a value you construct.
Level::parse · ::label · ::at_leastFields keep their type — null, bool, int, float, string, list, map, error — until a layout writes them.
Value::of_int · ::of_map · ::encodeOne event: level, target, message, ordered fields, tags, caller, thread, instant, sequence number.
Record::of · ::set · ::stampedwarn,app=debug,web:*=error. The most specific directive wins; a /regex narrows by message.
Eight steps: redact, sanitise, fields, match, exclude, sample, limit, dedupe. A step may drop the record.
Processor::process(record, env)A pattern, logfmt, a pretty block, or one of ten JSON schemas with keys renamed and fields nested.
Pattern · Logfmt · Pretty · JsonLineOne method: take an entry, answer Ok or say what went wrong. Nothing else is required.
One sink with its filter, steps, layout, colour, queue, retries, probe and failure policy around it.
Slot::new(sink) · ::at · ::queueUp, down or probing. It counts what it skipped, remembers what failed and waits before trying again.
Breaker::admit · ::fault · ::passTiers of sinks to try when one fails. * is every sink on the route; one success ends the walk.
The filter, the global chain, the slot table and the clock. Built by the application, held by nobody else.
Hub::new(clock~) · ::add · ::loggerA target and the fields bound to it. Passing one is how a library logs without knowing where it goes.
Logger::info · ::bound · ::enabledThe hub asks the clock it was given. A record carries the instant it was told, in nanoseconds; the health machine and the steps that sample, limit or dedupe take it as an argument. A test says which instant it means, and the same code runs at a different speed without changing.
Hub::new(clock=fn() { 1_700_000_000_000_000_000L })
There is no process-wide hub to configure and no default logger
to inherit. A module takes a Logger as a parameter
and writes through it; handed none, it writes nothing. Two hubs
can write the same file in two formats and never meet.
let sink = @moonlog.Memory::new() let hub = @moonlog.Hub::new( identity=@moonlog.Identity::of(app="gateway", env="prod"), clock=fn() { 1_700_000_000_000_000_000L }, ) let _ = hub.add( @moonlog.Slot::new(sink, name="console") .layout(@moonlog.Pattern::parse("{time:%H:%M:%S%.3f} {level:<5} {target}: {msg}{fields}")), ) let log = hub.logger("mirror::sync") log.info("starting", fields=[("workers", @moonlog.Value::of_int(4))]) // 22:13:20.000 INFO mirror::sync: starting workers=4
A write that fails moves its slot's breaker. While the breaker says
down, records for that slot are counted and dropped until
its next probe; a probe is one write deciding. Ten failures in a row
back off to five minutes, and a success puts everything back.
Nothing. The record is written to the sinks that are up, and the one that is down costs a count instead of a failed request.
The first failure opens an incident and an alarm walks its tiers: the other sinks, then the fallback. A reminder follows while it is still down; a resolution follows when it is not.
The incident is a record like any other — level, target, message, fields — so it reaches the same dashboards without a second alerting system.
| Schema | For | Shape |
|---|---|---|
| serlog | the default | time, level, target, msg, file, line, fields, app, env, instance |
| pino | pino | level by number, Unix ms, pid, hostname, fields, msg |
| bunyan | bunyan | name, hostname, pid, level, fields, msg, time, v |
| zap | zap's production encoder | level, ts, logger, caller, msg, fields |
| slog | Go's JSONHandler | time local with nanoseconds, slog's level labels |
| structlog | structlog's JSON renderer | event, fields, level, logger, timestamp |
| logstash | logstash-logback-encoder | @timestamp, @version, level, level_value, fields |
| ecs | Elastic Common Schema | dotted keys: log.level, service.name, process.pid |
| gelf | Graylog | short_message, syslog severity, fields prefixed _ |
| otel | OpenTelemetry logs | severity_number, body, attributes, resource |
One console on stdout, one on stderr, or one that picks per record. Colour is decided once, from FORCE_COLOR, NO_COLOR and the terminal.
A file that opens on its first line, cuts itself by size or by age, packs a segment as it goes, and keeps what a policy allows.
File::new · Rotation · LimitsEntries queue in the log call and ship from the async loop you already run: one stream per level, fields as structured metadata.
Loki::new · ::push · ::bodyA burst becomes one letter: a level floor, a window, a ceiling on records, a text part and a table in HTML.
Mail::new · ::send · ::draftYAML, JSON and TOML into one tree, every key checked against what its sink reads, a typo refused with its path.
Config::parse · ::load · ::dumpThe one process-wide hub, for applications only: SERLOG_FILTER, a zero-config console, and a flush before the process goes.