Contents
Getting started
- Install — Cargo, source, MSRV, features
- Quick Start — initialise and emit in ten lines
Library reference
- Why this approach? — design rationale
- Capabilities in 0.0.9 — release inventory
- The fluent API —
Log::info(...).with(...).fire() - Output formats — every
LogFormatvariant - Sinks —
os_log,journald, file, stdout - Configuration — TOML, env vars, hot-reload
- Log rotation — size, time, date, count
- Bridging existing facades —
log,tracing - Examples — runnable example index
Operational
- When not to use rlg — limitations
- Development — local verification, fuzzing, CI
- Security — guarantees and compliance
- Documentation — all reference docs
- License
Install
As a Rust library (crates.io)
[]
= "0.0.9"
Build from source
rlg targets Rust 1.88.0 (MSRV) and edition 2024. It runs
on macOS, Linux, and WSL; Windows is supported on a best-effort
basis via the stdout fallback sink.
Cargo features
All optional integrations are off by default. Enable only what the application needs.
| Feature | Pulls in | Adds | Documented in |
|---|---|---|---|
tokio |
tokio + notify |
Config::load_async, file-watcher hot-reload |
Configuration, examples/example_config.rs |
tui |
terminal_size |
Live terminal dashboard at RLG_TUI=1 |
Capabilities |
miette |
miette 7 |
Pretty diagnostic error reports | Library reference |
tracing-layer |
tracing-subscriber |
RlgLayer for composable tracing setups |
Bridging existing facades |
debug_enabled |
— | Verbose internal engine diagnostics | — |
# Example: async config loading + tracing bridge
[]
= { = "0.0.9", = ["tokio", "tracing-layer"] }
Quick Start
use Log;
use LogFormat;
Output (MCP / JSON-RPC 2.0 notification):
The default ingestion path runs in ~1.4 µs — Log::fire()
pushes a fully-built event into the crossbeam::ArrayQueue and
returns. A dedicated flusher thread picks the event up, runs the
chosen LogFormat's Display impl, and dispatches to the
configured PlatformSink.
Why this approach?
rlg targets the niche log / tracing / env_logger /
fern occupy — emit structured records from application
threads, route them somewhere durable — and is written
lock-free on the hot path against the LMAX Disruptor
pattern. The engine runs MIRI-clean under
-Zmiri-tree-borrows; 99.07 % of source lines and 99.30 % of
functions are covered by tests.
Two architectural choices motivate the design:
-
Atomic ingestion, deferred formatting.
Log::fire()only does the work that cannot be deferred: capturefile:linevia#[track_caller], increment the per-format metrics counter, and push into the ring buffer. The serialisation (fmt_json,fmt_mcp,fmt_otlp, …) and theos_log/journald/write_allsyscalls all run on the flusher thread, off the caller's critical path. The pattern that mainstream Rust loggers use — take a Mutex, format into a String, write to a Writer — is ~20 µs at p50 and pathologically variable under contention. rlg measures ~1.4 µs at p50 with no Mutex anywhere on the hot path. -
POSIX
syslog(3)for the macOS sink, not_os_log_impl. Apple'sos_logmacro expands into a binary-trailer calling convention that cannot be reproduced from Rust without inline assembly. Calling the private_os_log_implsymbol directly with raw bytes — as several Rust wrappers do — is undefined behaviour and crashes sporadically. rlg routes throughsyslog(3)instead, which on macOS Sierra+ is gateway'd intoos_logautomatically; records still appear in Console.app andlog stream, the ABI is stable, and the FFI surface is oneextern "C" fn syslog(c_int, *const c_char, *const c_char)call with a staticc"%s"format.
A few features built on top of those choices:
- 65k-slot ring buffer.
crossbeam_queue::ArrayQueue<LogEvent>with capacity tuned for typical service throughput; overflow evicts the oldest event and incrementsTuiMetrics::dropped. - Low-allocation serialisation.
session_idisu64;componentandtimeareCow<'static, str>so static strings stay on the stack;itoa/ryuformat integers and floats without allocating. - Native sinks, not file wrappers.
os_log(macOS viasyslog(3)) andjournald(Linux via the Unix datagram socket) integrate at the syslog protocol level —journalctl -u my-service, Console.app,log stream --predicate 'subsystem == "rlg"'all light up automatically.
The runtime default profile carries seven runtime crates
plus the well-vetted serde family. Disabling all optional
features keeps the engine compiling to the same seven; the
tokio runtime, terminal_size for the TUI, miette for
diagnostics, and tracing-subscriber for the layer bridge
are strictly opt-in.
Capabilities in 0.0.9
- 14 output formats. CLF, CEF, ELF, W3C, Apache Access,
Log4j XML, JSON, GELF, Logstash, NDJSON, MCP, OTLP, Logfmt,
ECS. Switch with
.format(LogFormat::X)per-entry or set the default viaRlgBuilder::format. - Native platform sinks.
os_logon macOS via POSIXsyslog(3);journaldon Linux via the/run/systemd/journal/socketUnix datagram; stdout and rotating files as fallbacks. - Background flusher thread. Single OS thread spawned at
init, drained on
FlushGuard::drop/ENGINE.shutdown().#[cfg_attr(miri, ignore)]on tests that spawn it; MIRI itself never sees the flusher. #[track_caller]everywhere it matters.Log::fire()recordsfile:linefor every entry. The caller string ends up in thecallerattribute alongside your other.with(...)keys.- AI-native formats. MCP is JSON-RPC 2.0 over
notifications/log— designed for Model Context Protocol agents (Claude Desktop, Cursor, mcp.run). OTLP maps to OpenTelemetry'sseverityNumber/severityText/spanId/traceIdso anotelcolpipeline picks up rlg records without an adapter. - TOML configuration with hot-reload.
Config::load_async- the
notifyfile watcher (behind thetokiofeature) picks up/etc/rlg.tomlmutations without a restart.
- the
- Bridges for
logandtracing.rlg::init()installs alog::Logimplementation; thetracing-layerfeature exposes atracing_subscriber::Layeryou can stack with the rest of your subscriber. - 99.07 % line coverage. Measured by
cargo llvm-cov. Run on every PR via the centralisedsebastienrousseau/pipelinesreusable workflows.
The fluent API
use Log;
use LogFormat;
error
.component
.with
.with
.with
.format
.fire;
| Method | Effect |
|---|---|
Log::info("…") |
Create at INFO level. Also: warn, error, debug, trace, fatal, critical, verbose. |
Log::build(level, "…") |
Create at an explicit LogLevel. |
.component("name") |
Tag the originating service or module. |
.with("key", value) |
Attach a key-value attribute. value: T: Serialize covers strings, numbers, booleans, arrays, structs. |
.time("…") |
Override the auto-captured ISO 8601 timestamp. |
.session_id(u64) |
Override the auto-assigned monotonic ID. |
.format(LogFormat::X) |
Pick the wire format for this entry. |
.fire() |
Consume the builder, capture file:line via #[track_caller], push into the ring buffer. |
Every method returns Self, so chains compose freely.
Output formats
The 14 variants of LogFormat cover most ingestion targets
without an adapter:
| Format | Use case | Example consumer |
|---|---|---|
MCP (default) |
LLM agent telemetry over JSON-RPC 2.0 | Claude Desktop, Cursor, mcp.run |
OTLP |
OpenTelemetry-native | otelcol, Honeycomb, Datadog OTel |
JSON |
Structured JSON for ingest pipelines | Vector, Fluent Bit, Loki |
NDJSON |
One record per line | Loki, ClickHouse |
ECS |
Elastic Common Schema | Elasticsearch, OpenSearch |
Logstash |
Logstash-flavoured JSON | Logstash, OpenSearch Pipelines |
GELF |
Graylog Extended Log Format | Graylog |
CEF |
Common Event Format | SIEMs (Splunk, ArcSight) |
CLF |
Common Log Format | nginx-style access logs |
ELF |
Extended Log Format | Legacy collectors |
W3C |
W3C Extended Log Format | Microsoft / legacy IIS |
ApacheAccessLog |
Apache combined log | Apache, awstats |
Log4jXML |
Log4j XML events | Java enterprise stacks |
Logfmt |
Human-readable key=value pairs |
Heroku, terminal viewing |
Switch per entry: .format(LogFormat::OTLP). Switch as a
process-wide default: RlgBuilder::format(LogFormat::JSON).
Sinks
PlatformSink::native() picks the best-available native sink
for the host. Override via Config::logging_destinations.
| Variant | Active when | Mechanism |
|---|---|---|
OsLog |
macOS | POSIX syslog(3) → routed into os_log by the system gateway. Visible in Console.app + log stream. |
Journald(Some(_)) |
Linux + systemd | Unix datagram to /run/systemd/journal/socket. journalctl shows records immediately. |
Journald(None) |
Linux without journald | Falls back to stdout. |
File(_) |
Explicit logging_destinations = [{ type = "file", path = "..." }] |
OpenOptions::new().create(true).append(true). |
Stdout |
Explicit fallback or RLG_FALLBACK_STDOUT=1 |
std::io::stdout().write_all. |
RLG_FALLBACK_STDOUT=1 (or GITHUB_ACTIONS=1) forces the
stdout sink — useful for CI runs and integration tests that
shouldn't pollute the real syslog.
Configuration
Config deserialises from TOML or environment variables (via
envy). All fields have serde defaults so an empty file or
unset environment is valid.
# rlg.toml
= "1.0"
= "production"
= "INFO"
= "%level - %message"
= [
{ = "file", = "/var/log/rlg.log" },
{ = "stdout" },
]
[]
= "size"
= 10485760 # 10 MiB
use Config;
// Sync load.
let cfg = load?;
// Async load + file-watcher hot-reload (requires `tokio` feature).
let cfg = load_async.await?;
hot_reload_async?;
| Field | Type | Notes |
|---|---|---|
version |
String |
Schema version. 1.0 is the only currently-accepted value. |
profile |
String |
Free-form deployment tag. |
log_level |
LogLevel |
Engine-wide filter level. Lower-severity records are dropped at ingest(). |
log_format |
String |
Default formatting template (CLF/Logfmt). |
logging_destinations |
Vec<LoggingDestination> |
Ordered list; PlatformSink::from_config picks the first openable one. |
log_rotation |
Option<LogRotation> |
See Log rotation. |
env_vars |
HashMap<String, String> |
${NAME} substituted from process env. |
The RUST_LOG environment variable is honoured too —
RlgBuilder::init overrides self.level with the most
permissive directive found (e.g. RUST_LOG=warn,my_crate=debug
yields DEBUG).
Log rotation
RotatingFile enforces a policy on a wrapped std::fs::File.
| Policy | Triggers on | Configured by |
|---|---|---|
Size(NonZeroU64) |
bytes written ≥ threshold | log_rotation = { type = "size", threshold = 10485760 } |
Time(NonZeroU64) |
seconds elapsed since open ≥ threshold | log_rotation = { type = "time", threshold = 3600 } |
Date |
local date string changes | log_rotation = { type = "date" } |
Count(u32) |
events written ≥ threshold | log_rotation = { type = "count", threshold = 10000 } |
On rotation the current file is renamed to
<stem>.<YYYYMMDD-HHMMSS>.<ext> and a fresh file is opened at
the original path.
Bridging existing facades
rlg::init() installs a global log::Log implementation —
existing code that calls log::info!, log::warn!, etc. is
re-routed through the rlg ring buffer with no source changes.
use ;
For tracing, enable the tracing-layer feature and stack
RlgLayer with your existing subscriber:
use *;
registry
.with
.with
.init;
RlgLayer mirrors every tracing::event! and span open/close
into the rlg ring buffer; span IDs surface as the span_id
attribute on emitted records.
Examples
Seven runnable examples ship under examples/:
| Example | Demonstrates | Required features |
|---|---|---|
example.rs |
End-to-end usage walkthrough (every public API) | none |
example_lib.rs |
Library-style embedding | none |
example_macros.rs |
The info! / warn! / error! macros |
none |
example_log_format.rs |
All 14 LogFormat variants side by side |
none |
example_log_level.rs |
Level filtering + LogLevel::includes semantics |
none |
example_config.rs |
TOML config + async loading + hot-reload | tokio |
example_utils.rs |
Datetime, span/trace ID, file utilities | tokio |
When not to use rlg
- You're emitting fewer than 100 records per second. The
ring buffer + background flusher pay a fixed setup cost
(one OS thread, ~256 KB ring) that only amortises at moderate
throughput. For low-throughput tools, a synchronous logger
like
env_loggeris simpler and just as fast at that volume. - You need stdout-only output and can tolerate Mutex
contention.
env_loggerandsimplelogare 50 lines of dependency; rlg's ring buffer is overkill if you don't need the platform sinks or the deferred formatting. - You need to log under MIRI. rlg's engine spawns a real
OS thread on
LockFreeEngine::newand the platform sinks call libc FFI. Most rlg tests are gated by#[cfg_attr(miri, ignore)]for that reason. For MIRI-clean inner-loop logging, write to aVec<u8>and inspect it at the end. - You need Windows Event Log as a native sink. rlg falls
back to stdout on Windows. A native ETW sink is on the
roadmap (
PlatformSink::WindowsEvent) but not yet implemented. - You want
tracing's span-based recording model as the primary API. Thetracing-layerfeature bridgestracingevents into rlg, but rlg's primary API is per-event (Log::info(...).fire()). If you need hierarchical spans as a first-class data model, usetracing-subscriber::fmtdirectly.
If you hit a case that should be on this list, please open an issue — that's how it gets fixed or moved into the supported set.
Development
On macOS, the engine routes through syslog(3) by default —
add RLG_FALLBACK_STDOUT=1 for tests that should never touch
the real system log.
CI
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml |
push, PR | Delegates to sebastienrousseau/pipelines/rust-ci.yml (Clippy, fmt, test matrix, coverage), security.yml (cargo-audit + dependency review), and docs.yml (deploy API docs on main). |
The reusable workflows live in the centralised
sebastienrousseau/pipelines
repo. See CONTRIBUTING.md for the
signed-commit policy and PR flow.
Security
Memory safety
unsafe_code = "deny"across the crate — the onlyunsafeblock is the macOSsyslog(3)FFI insrc/sink.rs, which is fully documented and gated behind#[cfg(target_os = "macos")].- The FFI surface is a single
extern "C" fn syslog(c_int, *const c_char, *const c_char)call with a staticc"%s"format string and exactly one argument — no varargs UB, no_os_log_impl-style private-symbol calls. - 99.07 % line coverage on the engine path, including the
concurrent queue retry, the shutdown idempotency, and the
OsLogpriority mapping.
Supply chain
cargo auditclean — zero advisories. RUSTSEC-2024-0436 (pasteunmaintained, viadtt) was closed in 0.0.9 by inlining the ISO 8601 helper.- Dependency count: 241 transitive crates. Down from 251
before the
dttremoval. - Signed commits enforced — see
SECURITY.mdfor the SSH-signing policy and reporting channel.
Reporting
Vulnerabilities go through the private channel documented in
SECURITY.md. Do not file public issues
for security problems.
Documentation
| Document | Covers |
|---|---|
doc/introduction.md |
Motivation and design overview. |
doc/tutorials/getting-started.md |
Step-by-step first integration. |
doc/how-to/fluent-api.md |
Building entries with the fluent builder. |
doc/explanation/engine-design.md |
LMAX Disruptor pattern as applied in rlg. |
doc/explanation/safety.md |
UB-free FFI design, MIRI posture. |
SECURITY.md |
Disclosure policy, supported versions, contact. |
CONTRIBUTING.md |
Signed-commit policy, PR guidelines, local-test recipe. |
API documentation is published at docs.rs/rlg on every release.
License
Dual-licensed under Apache 2.0 or MIT, at your option.