penna 0.1.0

Structured JSON logging for tracing, in one line per event, without a regex engine underneath
Documentation
  • Coverage
  • 100%
    15 out of 15 items documented1 out of 9 items with examples
  • Size
  • Source code size: 51.6 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 529.8 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 3s Average build duration of successful builds.
  • all releases: 3s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • bas3line/penna
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • bas3line

penna

crates.io docs.rs ci

Structured JSON logging for tracing, one line per event, with three crates in the tree instead of fifteen.

penna::json().install();

tracing::info!(events = 5000, backup_key = "…", "telemetry events persisted");
{"timestamp":"2026-09-21T00:56:59.991167Z","level":"INFO","fields":{"message":"telemetry events persisted","events":5000,"backup_key":"…"},"target":"my_service"}

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

[dependencies]
penna = "0.1"
fn main() {
    penna::json().install();            // filter from RUST_LOG, default "info"
    // …
}

Set the filter directly, or write somewhere other than stdout:

penna::json().filter("warn,my_service=debug").install();
penna::json().with_writer(penna::Stderr).install();

Build it without installing, for tests or for wrapping:

let subscriber = penna::json().filter("info").finish();
tracing::subscriber::with_default(subscriber, || {
    tracing::info!("scoped to this closure");
});

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.

{"timestamp":"…","level":"INFO","fields":{"message":"inside"},"target":"my_service","span":{"worker":3,"name":"delivery"},"spans":[{"worker":3,"name":"delivery"}]}

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.