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.
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.