Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 APILog::info("msg").with("key", val).fire()
  • Platform-native sinks — macOS os_log, Linux journald, file, stdout
  • log and tracing bridges — 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.
  • 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:

  1. Log::info("msg").fire() builds a LogEvent and calls ENGINE.ingest().
  2. ingest() checks the event’s level against an atomic filter. Events below the threshold are dropped immediately.
  3. ingest() pushes the event into the ArrayQueue. If the buffer is full, it evicts the oldest entry and retries.
  4. ingest() unparks the flusher thread via a cached std::thread::Thread handle — no Mutex on 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:

  1. Drain all available events from the queue into a local batch.
  2. Format each event into a reusable String buffer using Display::fmt.
  3. Write the batch to the configured sink (file, journald, os_log, or stdout).
  4. 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:

PlatformSinkMechanism
macOSos_logFFI call to libsystem
LinuxjournaldUnixDatagram to /run/systemd/journal/socket
FallbackFile / stdoutstd::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:

  1. Drains all remaining events from the queue.
  2. Joins the flusher thread.
  3. 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_log or socket calls never escape their valid region.
  • Alignment errors — stack-allocated itoa buffers 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

GuaranteeMechanism
No data racescrossbeam::ArrayQueue + atomics
No use-after-free in FFICString lifetime outlives every call
No provenance violationsMIRI -Zmiri-tree-borrows on every CI run
No alignment faultsitoa/ryu stack buffers verified by MIRI
No lock contentionFlusher thread unparked via cached Thread handle