std.log
Leveled logging with pluggable sinks.
import std.log
log.info("started", { port: 8080 })
log.warn("retrying", { attempt: 2 })
log.error("failed", { err: e.message })
log.debug("cache hit", { key: k })
Output goes to stderr by default, so it does not mix into your program's stdout - which is what keeps a program's actual output pipeable and testable.
Levels
debug < info < warn < error. ECKO_LOG sets the filter, default info:
ECKO_LOG=debug ecko app.ecko
Structured fields
The second argument is a map, and fields print in sorted order - so log lines are stable and greppable:
[INFO] started port=8080
Fields beat interpolation for anything you will later search or aggregate: port=8080 is findable in a way that "started on port 8080" is not.
Sinks
log.to_file("app.log", level: "warn", json: true, max_bytes: 1000000, keep: 5)
log.to_stderr(json: true)
log.configure({ sinks: [
{ target: "stderr", level: "info" },
{ target: "file", path: "app.log", json: true },
]})
log.reset() # back to the single default stderr sink
Each sink has its own level, its own format (json gives one JSON object per line instead of [LEVEL] msg k=v), and size-based rotation via max_bytes and keep - rolling app.log to app.log.1 and so on.
Where a line came from
location: true makes a sink name the call site of each line, as file:line:col
- the same form an error uses:
log.to_stderr(location: true) log.info("charged", { order: 1042 })[INFO] charged order=1042 at=billing.ecko:2:9
In JSON the location is an "at" field after "msg". A call inside an imported module names that module's file. A field you name at yourself is kept, and the location is left out of that line. Only sinks that ask for it pay for it; the default stderr sink does not, so existing output is unchanged.
A common shape: human-readable at info on stderr, JSON at warn to a file for whatever collects it. File sinks need fs:write for package code.
No automatic timestamp
Deliberate, so log output is deterministic and a test can assert on it. Add one when you want it:
log.info("started", { at: time.now_iso() })
In production, whatever collects your logs almost certainly stamps them on arrival anyway.
Secrets are redacted
A secret renders as [secret] through log sinks like everywhere else, so a credential in a field map cannot land in a log file. A revealed value will.