log-io 2.0.0

Structured logging pipeline for Rust: filter, format, sink. Allocation-free hot path, JSON / logfmt / human output that is safe for any input, thread and async context propagation, per-target filtering, durable shutdown, background and rotating file sinks, plus optional log and tracing bridges.
Documentation

Features

Pipeline

  • Three formats - JSON Lines, logfmt, and an aligned human-readable line with optional terminal colour.
  • Per-target filtering - RUST_LOG-style directives where the most specific rule wins; read them from an environment variable with env_filter.
  • Ambient fields - service-level default fields and thread-local context (trace_id, request_id), de-duplicated with clear precedence.
  • Several sinks per logger - stdout/stderr, buffered files, size- and time-based rotation, any io::Write, and a background writer with a bounded queue.

Ergonomics

  • Macros with format! messages - info!("user {id} logged in", http.status = 200, peer = %addr), with or without an explicit logger.
  • Global logger - log_io::init(logger) returns a guard that flushes at the end of main.
  • Async-friendly context - capture context once and re-install it around every poll of a future, on any executor.
  • log and tracing bridges - route existing ecosystem logging into log-io sinks (features log and tracing).

Safety and reliability

  • Injection-safe output - C0/C1 controls, DEL, U+2028/2029 and bidi controls are escaped in every format; metadata keys cannot be spoofed by fields.
  • Redaction - replace password, token, authorization and other configured keys with [REDACTED] at the logging boundary.
  • Durable shutdown - flush-on-level (errors by default), flush guards, fsync on flush when you need it, and an opt-in panic hook.
  • Zero unsafe code - #![forbid(unsafe_code)]; sinks recover from mutex poisoning; library code never unwraps.

Installation

[dependencies]
log-io = "2"

# Optional integrations:
log-io = { version = "2", features = [
    "log",      # route `log` macros into log-io (LogBridge)
    "tracing",  # use log-io as a tracing-subscriber layer (TracingLayer)
] }

Feature recommendations

  • Applications: the defaults (std, json, logfmt, human).
  • Libraries already using log or tracing: add log or tracing and keep your existing macros.
  • Embedded (no_std): default-features = false, features = ["json"] (or logfmt / human) for the data model and formatters only.

Quick Start

use log_io::{Level, Logger};

let logger = Logger::builder()
    .level(Level::Info)
    .env_filter("LOG_IO") // e.g. LOG_IO="info,my_app::db=debug"
    .with_default_field("service", "billing")
    .stdout_json()
    .build();
let _guard = log_io::init(logger).expect("first and only global logger");

let port = 8080_u16;
log_io::info!("listening on port {port}", tls = true);
log_io::warn!(target: "my_app::db", "slow query", ms = 412_u64, table = "users");

Context across threads and async tasks

use log_io::{context, Logger};

let logger = Logger::builder().stdout_logfmt().build();
let _request = context::with_request_id("req-7");
log_io::info!(logger, "accepted"); // carries request_id=req-7

let snapshot = context::capture();
let worker = logger.clone();
std::thread::spawn(move || {
    snapshot.scope(|| log_io::info!(worker, "processing")); // carries it too
})
.join()
.unwrap();
// For futures: tokio::spawn(snapshot.wrap(async move { ... }))

Durable file output off the hot path

use log_io::format::JsonFormat;
use log_io::sink::{NonBlocking, RotatingFile, Rotation};
use log_io::{Level, Logger};

let file = RotatingFile::builder("logs/app.jsonl")
    .max_bytes(50 * 1024 * 1024)
    .rotation(Rotation::Daily)
    .max_files(14)
    .open()?;
let sink = NonBlocking::new(file, JsonFormat::new())?;
let logger = Logger::builder()
    .level(Level::Info)
    .redact_common_secrets()
    .with_sink(sink)
    .build();
let _guard = log_io::init(logger)?; // drains the queue when main returns
# Ok::<(), Box<dyn std::error::Error>>(())

Plugging into log and tracing

use log_io::compat::{LogBridge, TracingLayer};
use log_io::Logger;
use tracing_subscriber::layer::SubscriberExt;

let logger = Logger::builder().stdout_logfmt().build();
LogBridge::new(logger.clone()).install().ok();
log::info!(user = "alice"; "from the log facade");

let subscriber = tracing_subscriber::registry().with(TracingLayer::new(logger));
tracing::subscriber::with_default(subscriber, || {
    let _span = tracing::info_span!("request", id = 7_u64).entered();
    tracing::info!(status = 200_u16, "from tracing"); // carries id=7
});

Performance

Measured on an AMD Ryzen 9 9950X3D, Windows 11, rustc 1.95; see BENCH.md for the method and the full tables.

Workload 2.0.0 vs 1.0.0
JSON format, 5 fields ~123 ns 8% faster
logfmt format, 5 fields ~115 ns 26% faster
Pipeline, JSON writer, 3 fields ~64 ns same
Pipeline, default config (timestamp + context) ~114 ns same
Record rejected by the filter ~1 ns 60% faster
4 threads, one shared JSON sink ~24 ns/record 25% faster
8 threads, one shared JSON sink ~27 ns/record 2.7x faster

No heap allocation per record in steady state (enforced by tests/alloc.rs).


Status: Stable

Current version: 2.0.0. The API follows Semantic Versioning; Value, Error, Metadata, Record and the configuration enums are #[non_exhaustive] so they can grow in minor releases. Upgrading from 1.x: see docs/MIGRATION.md.

Planned: sampling and rate limiting per call site, OpenTelemetry-shaped JSON layouts, compression of rotated files.


Documentation & Resources

Documentation

Examples

Example Shows
01_quick_start the smallest useful setup
02_macros format strings, dotted keys, % / ? values, targets
03_context request context and snapshots across threads
04_filter directives, most-specific matching, LOG_IO env var
05_file_sink durable file output with a flush guard
06_multi_sink human on stderr and JSON in a file
07_default_fields_and_redaction service fields and secret redaction
08_custom_format custom and runtime-selected formats
09_custom_sink implementing Sink
10_non_blocking background writer with rotation
11_log_bridge the log facade (feature log)
12_tracing_layer tracing with span fields (feature tracing)

Run one with cargo run --example 02_macros (add --features log or --features tracing for the last two).


Troubleshooting


Version Compatibility

  • Rust: 1.85+ (edition 2024). Raising the MSRV is a major-version change.
  • API stability: semantic versioning; see REPS.md section 9.
  • Feature flags: additive; log and tracing are independent of each other.

Development Setup

git clone https://github.com/jamesgober/log-io.git
cd log-io
cargo test --all-features    # unit, integration, property, doc tests
cargo bench --all-features   # criterion benchmarks
cargo clippy --all-targets --all-features -- -D warnings
cargo +nightly fuzz run json_format   # see fuzz/README.md