penna
Structured JSON logging for tracing, one line per
event, with three crates in the tree instead of fifteen.
json.install;
info!;
Why
A service that logs JSON lines to stdout and filters them by level needs a
timestamp, a string escaper, and a comparison against a target prefix. The
usual way to get that is tracing-subscriber, which brings a regex engine, an
ANSI colour library, a sharded slab and a thread-local crate — none of which a
JSON line ever touches.
tracing-subscriber (json, env-filter) |
penna |
|
|---|---|---|
| Crates in the tree | 15 | 3 |
| Of those, not already yours | 12 | 1 |
penna's three are itself, tracing-core — which defines the traits, and
which you already have if you use tracing — and once_cell, which
tracing-core brings. There is no regex engine, no colour support, and no
serialisation framework: a tracing field arrives already typed, so each one
is written straight out as a "key":value pair.
The line shape is tracing-subscriber's JSON formatter's, key for key, so a
dashboard or log pipeline already parsing those lines keeps working.
Use
[]
= "0.1"
Set the filter directly, or write somewhere other than stdout:
json.filter.install;
json.with_writer.install;
Build it without installing, for tests or for wrapping:
let subscriber = json.filter.finish;
with_default;
Filtering
RUST_LOG directives, in the form almost everyone uses:
info everything at info and above
warn,my_service=debug debug for one module, warn elsewhere
my_service::spool=trace one module, everything else off
A directive is a target prefix and a level. Matching is by path prefix on
module boundaries — my_service matches my_service::spool but not
my_service_other — and the longest matching prefix wins, so a specific
directive beats a general one whatever order they appear in.
Spans
Open spans ride along on the events inside them: the innermost as span, the
whole stack as spans, each with the fields it was created with. Nothing is
printed when a span opens or closes, because a span is context for its events
rather than an event.
What it does not do
No regex or field matching in the filter, no per-span filtering, no colours,
no non-JSON formats, no log rotation, and no log crate bridge. If you want
any of those, tracing-subscriber is the right tool and this is not trying to
replace it — only to be enough for a service that writes JSON lines and greps
them later.
How you know it is right
Every line is parsed back and checked field by field: levels, targets,
messages, each field type, an error's source chain, quotes and newlines and
unicode, nested spans, and that a closed span never leaks into a later event.
String escaping is compared against serde_json's own output, and eight
threads logging at once must produce four hundred whole lines. Date formatting
is checked at the epoch, on leap days, and across the century rule.
Minimum supported Rust
1.75.
License
MIT or Apache-2.0, at your option.