Structured logging for MoonBit.
let sink = @moonlog.Lines::new(fn(line) { println(line) })
let hub = @moonlog.Hub::new(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))])
log.warn("slow", fields=[("took_ms", @moonlog.Value::of_int(1200))])
// 22:13:20.000 INFO mirror::sync: starting workers=4
// 22:13:20.000 WARN mirror::sync: slow took_ms=1200moon add moonbitstack/moonlogflowchart LR
L[Logger<br/>target + fields] --> H[Hub<br/>filter + chain]
H --> F{slot filter}
F -- dropped --> X(( ))
F -- kept --> P[slot chain]
P -- dropped --> X
P -- kept --> Y[Layout<br/>pattern · logfmt · pretty · JSON]
Y --> Q{queue?}
Q -- direct --> S[Sink]
Q -- buffered --> S
S -- ok --> B[Breaker<br/>up]
S -- failed --> B2[Breaker<br/>down]
B2 --> A[Alarm<br/>tiers · * · fallback]
A --> D[other sinks]| Level | Value | Colour | Written as |
|---|---|---|---|
| trace | 10 | cyan | TRACE |
| debug | 20 | magenta | DEBUG |
| info | 30 | green | INFO |
| warn | 40 | yellow | WARN |
| error | 50 | red | ERROR |
| fatal | 60 | white on red | FATAL |
| Type | What it carries |
|---|---|
| Value | Null, Bool, Int, Float, Str, List, Map, Error(message, causes), Other(text) |
| Caller | module path, function, file, line — every part optional |
| Record | level, target, message, ordered fields, tags, caller, thread, span chain, instant, sequence number |
| Identity | app, env, instance, host, pid, start, and the offset local time is written with |
| Directive | Meaning |
|---|---|
| info | the default level |
| app=debug | target app and everything under it |
| app | everything for app |
| app=off | nothing for app |
| web:*=error | * matches any run of characters |
| info/sync(ed)? | the message must also match the expression after / |
| 35 | a level by number |
| Step | Options | Effect |
|---|---|---|
| Redact | key, path, pattern, mask | masks fields by name at any depth, at a dotted path, and matches in the message |
| Sanitise | — | escapes line breaks, tabs and control characters as \n, \r, \t, \u{0007} |
| Fields | key-value pairs | sets fields on every record |
| Keep | tag, field, level | keeps a record only when every condition holds |
| Exclude | the same | drops a record when every condition holds |
| Sample | first, every, per | per level and message, keeps the first first in each per, then one in every |
| Limit | rate, burst | a token bucket of rate a second holding up to burst |
| Dedupe | within | drops repeats within the window; the next one kept carries repeated |
| Layout | Output |
|---|---|
| Pattern | a template, or a preset name |
| Logfmt | time=… level=warn target=app msg="…" key=value |
| Pretty | a first line, then one aligned key = value per field |
| JsonLine | one object in a schema, with keys renamed, levels by name or number, fields nested |
| Preset | Reproduces | Pattern |
|---|---|---|
| serlog | the default | {time} {level:<5} {target}: {msg}{fields} |
| tracing | tracing-subscriber | {time:rfc3339} {level:>5} {spans}{target}: {msg}{fields} |
| env_logger | env_logger | [{time:utc} {level:<5} {target}] {msg} |
| python | logging.basicConfig | {level}:{target}:{msg} |
| logback | Logback, Log4j 2 | {time:%H:%M:%S%.3f} [{thread}] {level:<5} {target:~36} - {msg} |
| slog | Go's slog.TextHandler | time={time:%Y-%m-%dT%H:%M:%S%.3f%:z} level={level} msg={msg:q}{fields:q} |
| pino | pino-pretty | [{time:%H:%M:%S%.3f}] {level} ({pid}): {msg}{fields} |
| Token | Options | Writes |
|---|---|---|
| time | local, utc, rfc3339, unix, unix_ms, unix_ns, or strftime | the instant |
| level | lower | the level, as the profile or the preset spells it |
| target | ~N shortens leading segments until it fits | where the record came from |
| module, func, file, line | file:base keeps the name | the caller, ? when unknown |
| thread, tid, task | where it ran | |
| pid, host, app, env, instance | the identity | |
| msg, fields | q quotes the logfmt way | the message, and key=value per field |
| tags, error, spans | tags, the error field with its causes, the span chain | |
| elapsed, seq | milliseconds since the identity's start, the record's number |
| Schema | Keys |
|---|---|
| serlog | time level target msg file line fields app env instance |
| pino | level time pid hostname fields msg |
| bunyan | name hostname pid level fields msg time v |
| zap | level ts logger caller msg fields |
| slog | time level msg fields |
| structlog | event fields level logger timestamp |
| logstash | @timestamp @version message logger_name thread_name level level_value fields |
| ecs | @timestamp log.level message ecs.version log.logger log.origin.* process.pid host.hostname service.* fields |
| gelf | version host short_message timestamp level _target fields, each prefixed _ |
| otel | timestamp severity_text severity_number body attributes resource |
| Setting | Default | What it does |
|---|---|---|
| name | — | how an alarm names it; * is refused |
| kind | custom | what the sink is, for a status page |
| filter / at | the hub's filter alone | what reaches this sink |
| process | none | steps that run for this slot only |
| layout | the profile's pattern | how entries are written |
| coloured | false | whether the line carries escapes |
| queue | direct | size, batch, wait, full: Drop or Block |
| retries | 0 | how many more times a failed write is tried |
| probe | 5 s | first wait before probing a failed sink, doubling to 5 min |
| on_fail | Ignore | Panic, Ignore, or Alarm(tiers) |
| alarm_only | false | keep it off the log route |
stateDiagram-v2
[*] --> Up
Up --> Down: write failed
Down --> Down: record skipped
Down --> Probing: probe due
Probing --> Up: write went through
Probing --> Down: write failed againlet oncall = @moonlog.Tiers::from([["loki"], ["mail"], ["stderr"]])
.with_fallback(@moonlog.Fallback::Ignore)
.with_remind(1_800_000_000_000L) // half an hour
let _ = hub.add(
@moonlog.Slot::new(sink, name="loki").on_fail(@moonlog.OnFail::Alarm(oncall)),
)| Profile | Console | Level | Filter variable |
|---|---|---|---|
| serlog | stdout | info | SERLOG_FILTER |
| tracing | stdout | info | RUST_LOG |
| env_logger | stderr | error | RUST_LOG |
| python | stderr | warn | — |
| logback | stdout | debug | — |
| slog | stdout | info | — |
| pino | stdout | info | — |
| Example | What it shows |
|---|---|
| 01-tour | a hub, two slots, a filter, a logger with a bound field, and one record in six shapes |
| 02-formats | every preset, every schema, renamed keys, a coloured line |
| 03-service | a service: redaction, sampling, a queue, a sink that is down, an alarm that falls back |
moon run src/examples/01-tourpub(all) suberror Refused {
Refused(at~ : Int, got~ : String, want~ : String)
}pub struct Breaker {
probe : Int64
health : Health
gap : Int64
next : Int64
told : Int64?
since : Int64
opened : Int64
opened_at : Int64
skipped : Int64
error : String?
}pub struct Discard {
}pub(all) struct Env {
now : Int64
}pub(all) struct Failure {
sink : String
error : String
at : Int64
}pub(all) struct Identity {
app : String
env : String
instance : String
host : String
pid : Int
start : Int64
offset : (Int64) -> Int
}pub struct Limit {
rate : Double
burst : Double
level : Double
at : Int64
started : Bool
}pub struct Lines {
write : (String) -> Unit
}pub struct Logfmt {
}pub struct Pretty {
}type Rulepub struct Sanitise {
}pub struct Slot {
name : String
kind : String
sink : &Sink
filter : Filter?
chain : Array[&Processor]
layout : &Layout
layout_given : Bool
colour : Bool
alarm_only : Bool
on_fail : OnFail
queue : Queue?
retries : Int
breaker : Breaker
dropped : Int64
suppressed : Int64
buffered : Array[Record]
buffered_at : Int64
}pub(all) struct Status {
name : String
kind : String
health : Health
skipped : Int64
dropped : Int64
suppressed : Int64
since : Int64
error : String?
}let batch_default : Intlet longest_probe : Int64let probe_default : Int64let remind_default : Int64let wait_default : Int64