RLG — RustLogs
A high-performance structured logging library for Rust.
RLG pushes log events into a lock-free ring buffer and formats them on a background thread. Your application thread never blocks on I/O.
Core Features
- 14 output formats — JSON, NDJSON, OTLP, MCP, GELF, CEF, ECS, Logfmt, CLF, W3C, Syslog, Logstash, Log4j-XML, Apache Error
- Fluent builder API —
Log::info("msg").with("key", val).fire() - Platform-native sinks — macOS
os_log, Linuxjournald, file, stdout logandtracingbridges — drop-in replacement for existing Rust logging- TUI dashboard — real-time throughput and error metrics in-terminal
- Log rotation — size, time, date, or count-based policies
Quick Start
[dependencies]
rlg = "0.0.7"
use rlg::init;
use rlg::log::Log;
fn main() {
let _guard = init::init().expect("failed to initialise RLG");
Log::info("Service started")
.with("version", "0.0.7")
.fire();
}
// FlushGuard drops here — all buffered events flush automatically.
Navigation
- Getting Started — install, configure, and emit your first log
- Fluent API — chain
.with(),.component(),.format(), then.fire() - Engine Design — how the ring buffer and background flusher work
- Safety — MIRI verification and FFI boundary guarantees
- API Reference — auto-generated Rustdoc
Getting Started
Install RLG, emit your first log, and verify output — all in under five minutes.
1. Add the Dependency
[dependencies]
rlg = "0.0.7"
For OTLP streaming to Grafana Loki or similar collectors, enable the reqwest feature:
rlg = { version = "0.0.7", features = ["reqwest"] }
2. Initialise and Log
Call init::init() once at startup. Store the returned FlushGuard — dropping it flushes all buffered events and shuts down the background thread.
use rlg::init;
use rlg::log::Log;
use rlg::log_format::LogFormat;
fn main() {
let _guard = init::init().expect("failed to initialise RLG");
Log::info("System initialisation complete")
.component("kernel")
.with("version", "0.0.7")
.format(LogFormat::JSON)
.fire();
}
fire() pushes the event into a ring buffer and returns immediately. The background flusher thread handles formatting and I/O.
3. Enable the TUI Dashboard
Set RLG_TUI=1 to display a live metrics dashboard in your terminal:
RLG_TUI=1 cargo run
The dashboard shows throughput, error rates, active spans, and format distribution at 60 FPS.
4. Verify Platform-Native Output
RLG routes logs to your OS-native sink automatically:
-
macOS — appears in Console.app via
os_log:log show --predicate 'subsystem == "com.rlg.logger"' --last 1m -
Linux — appears in the systemd journal via
journald:journalctl -t rlg --since "1 min ago"
If neither sink is available, RLG falls back to the configured file path or stdout.
Next Steps
- Fluent API — chain
.with(),.component(),.format(), then.fire() - Engine Design — how the ring buffer and flusher thread work
How-To: The Fluent API
Build structured log entries with a chainable builder. Every method returns Self — chain freely, then dispatch with .fire().
1. Start with a Severity Level
Every log begins with a level shortcut. This returns a builder with sensible defaults.
#![allow(unused)]
fn main() {
use rlg::log::Log;
Log::info("Connection established").fire();
}
Available shortcuts: info, warn, error, debug, trace, fatal, critical, verbose.
2. Attach Structured Context
Add key-value attributes with .with(). Accepts any T: Serialize.
#![allow(unused)]
fn main() {
Log::warn("Potential breach detected")
.with("ip_address", "192.168.1.100")
.with("attempts", 5)
.with("target_resource", "/admin/login")
.fire();
}
Attributes are stored in a BTreeMap<String, serde_json::Value> and serialized in sorted order.
3. Override Component and Format
Tag the originating module with .component(). Switch the output format per-entry with .format().
#![allow(unused)]
fn main() {
use rlg::log_format::LogFormat;
Log::error("Database query failed")
.component("db-client-pool")
.format(LogFormat::OTLP)
.with("query_time_ms", 1250)
.fire();
}
4. Manual Control
.fire() consumes the builder and pushes it into the ring buffer. For deferred dispatch, store the builder and fire later.
#![allow(unused)]
fn main() {
let entry = Log::info("Ready")
.session_id(42)
.time("2026-03-05T12:00:00Z");
// ... additional processing ...
entry.fire();
}
.fire() vs .log(): .fire() consumes self (no clone). .log() borrows and clones — use it only when you need to retain the entry.
AI Format Guidelines
For LogFormat::MCP and LogFormat::OTLP, use descriptive snake_case keys in .with(). AI orchestrators map these keys automatically for anomaly detection and state tracking.
Engine Design
RLG separates log ingestion from formatting and I/O. Application threads push events into a ring buffer; a single background thread drains, formats, and writes them.
1. The Ring Buffer
The engine uses a crossbeam::ArrayQueue with a fixed capacity of 65,536 slots. ArrayQueue is a bounded, multi-producer, multi-consumer queue backed by contiguous memory and atomic operations.
Call flow:
Log::info("msg").fire()builds aLogEventand callsENGINE.ingest().ingest()checks the event’s level against an atomic filter. Events below the threshold are dropped immediately.ingest()pushes the event into theArrayQueue. If the buffer is full, it evicts the oldest entry and retries.ingest()unparks the flusher thread via a cachedstd::thread::Threadhandle — noMutexon the hot path.
2. The Flusher Thread
A single OS thread (std::thread::spawn) parks itself when the queue is empty and unparks on each ingest(). On wake:
- Drain all available events from the queue into a local batch.
- Format each event into a reusable
Stringbuffer usingDisplay::fmt. - Write the batch to the configured sink (file, journald, os_log, or stdout).
- Park again.
The flusher reuses its format buffer across batches to avoid repeated heap allocation.
3. Deferred Formatting
Formatting happens on the flusher thread, never on the caller’s thread. Log::build() captures metadata (level, description, component, attributes) without serialising to a string. The Display implementation on Log handles serialisation when the flusher calls write!.
This design keeps the ingestion path fast: one atomic level check, one ArrayQueue::push, one thread::unpark.
4. Platform Sinks
The flusher dispatches formatted output to a PlatformSink:
| Platform | Sink | Mechanism |
|---|---|---|
| macOS | os_log | FFI call to libsystem |
| Linux | journald | UnixDatagram to /run/systemd/journal/socket |
| Fallback | File / stdout | std::fs::File or std::io::stdout |
Sink selection happens once at startup via PlatformSink::from_config() or PlatformSink::native().
5. Shutdown
Call ENGINE.shutdown() or drop the FlushGuard returned by init(). This:
- Drains all remaining events from the queue.
- Joins the flusher thread.
- Closes the sink.
If you exit without shutdown, buffered events are lost. Always hold the FlushGuard until process exit.
Safety: MIRI and FFI Guarantees
RLG interfaces with OS kernels via C-FFI for os_log (macOS) and journald (Linux). This page documents the verification strategy and safety boundaries.
1. Lock-Free Concurrency
The engine uses crossbeam::ArrayQueue instead of Mutex<T>. Multiple application threads push events concurrently; a single flusher thread drains them. Memory visibility relies on atomic acquire/release semantics — no locks on the hot path.
2. MIRI Verification
Every CI run executes the full test suite under MIRI, the Rust MIR interpreter:
MIRIFLAGS="-Zmiri-tree-borrows" cargo miri test
MIRI checks for:
- Pointer provenance violations — pointers passed to
os_logor socket calls never escape their valid region. - Alignment errors — stack-allocated
itoabuffers meet CPU-native alignment requirements. - Data races — no two threads access mutable memory without proper synchronisation.
Tests that spawn OS threads or touch real sockets are #[cfg_attr(miri, ignore)] — MIRI cannot emulate kernel syscalls.
3. FFI Boundaries
macOS os_log
#![allow(unused)]
fn main() {
// SAFETY: `subsystem` and `category` are valid, null-terminated CStrings.
// Their lifetimes outlive the FFI call.
unsafe {
let handle = os_log_create(subsystem.as_ptr(), category.as_ptr());
}
}
Every unsafe block carries a // SAFETY: comment documenting the invariant it relies on.
Linux journald
The Linux sink uses safe Rust (UnixDatagram). The binary payload follows the systemd native protocol specification. No unsafe is required.
4. Stack-Based Formatting
The flusher formats numeric values with itoa (integers) and ryu (floats) — both write to stack buffers, avoiding heap allocation. The format buffer itself is a reusable String that grows once and persists across flush cycles.
This reduces the surface area for OOM conditions under sustained high throughput.
5. Summary
| Guarantee | Mechanism |
|---|---|
| No data races | crossbeam::ArrayQueue + atomics |
| No use-after-free in FFI | CString lifetime outlives every call |
| No provenance violations | MIRI -Zmiri-tree-borrows on every CI run |
| No alignment faults | itoa/ryu stack buffers verified by MIRI |
| No lock contention | Flusher thread unparked via cached Thread handle |