Expand description
Ticklog is a fast, minimal logging library for latency-critical Rust applications such as high-frequency trading, where the cost of a log call on the hot path must stay in the low tens of nanoseconds.
§How it works
A logging macro runs entirely on the calling thread’s hot path: it checks the level, then encodes a compact binary record into that thread’s private lock-free buffer and returns. Format and source strings are stored by pointer and arguments in their native form, so no text formatting happens here.
A background drain thread does the rest: it decodes each record, formats
and timestamps it, and writes the text to a LogSink, keeping all of that
cost off the calling thread.
§Benchmarks
Per-call latency on a Mac (M4, macOS 15, Rust 1.85, release profile).
Each measurement is the median of 200 criterion samples. Lower is better.
| Logger | info!(“x={}”, 42u64) | info!(“{}”, “hello world”) | info!(“{} {} {}”, 42u64, 3.14159, “hello world”) |
|---|---|---|---|
| ticklog | 8.0 ns | 9.7 ns | 11.6 ns |
| env_logger | 231 ns | 232 ns | 307 ns |
| slog | 274 ns | 269 ns | 454 ns |
| tracing | 386 ns | 425 ns | 458 ns |
§Quick start
use ticklog::{info, FileSink};
// Build the logger, then keep the returned guard alive for as long as you
// want to log.
let _guard = ticklog::builder()
.sink(FileSink::new("app.log").unwrap())
.build()
.unwrap();
info!("listening on {}", 8080);Builder::build may be called only once per process and returns a
Guard. Hold the guard for as long as you want to log: when it is
dropped it flushes the sink, stops the background drain thread, and disables
logging, so every log call afterwards is a silent no-op.
§Logging macros
trace!, debug!, info!, warn!, and error! take a format
string literal followed by positional arguments:
info!("connected to {}", "db-01");
error!("request {} failed after {} retries", 42, 3);Placeholders are {} (Display) and {:?} (Debug). The number of
placeholders is checked against the number of arguments at compile time,
so a mismatch is a build error rather than a runtime surprise.
§Configuration
builder returns a Builder with these knobs:
sink: where output goes. Defaults to aConsoleSinkon stderr (colored when stderr is a terminal).max_level: the level ceiling; records above it are dropped on the hot path before any encoding. Defaults toLevel::Info.backpressure: what a logging thread does when its buffer is full, eitherBackpressure::Drop(the default, never blocks) orBackpressure::Block(spin until space frees up).timezone_offset: offset applied when formatting timestamps. Defaults to UTC.drain_affinity: pin the drain thread to a set of logical CPUs.
§Sinks
A LogSink is the final destination for formatted lines. The crate ships:
ConsoleSink: stdout or stderr, with automatic or forced ANSI coloring by level (seeColorMode).FileSink: a buffered single file, opened in append or truncate mode.WriterSink: wraps anystd::io::Write; the escape hatch for custom destinations such as rotating files or the network.
Compose and filter sinks with FanOut (dispatch one record to several
sinks) and LogSinkExt::with_max_level (limit a sink to a level and below):
use ticklog::{ConsoleSink, FanOut, Level, LogSinkExt};
let sink = FanOut::new()
.add(ConsoleSink::stderr().with_max_level(Level::Warn))
.add(ConsoleSink::stdout().with_max_level(Level::Info));§Threads
Any thread may log, and each allocates its own buffer on first use. To
move that one-time allocation off a latency-sensitive path, call warm_up
on the thread before its first log call.
§Safety
The public API contains no unsafe functions. Internally, unsafe is
confined to three areas, each with a documented invariant:
- Thread-local buffer access: per-thread buffers live in an
UnsafeCellguarded by a re-entrancy flag. A re-entrant log call on the same thread is detected and refused before it can form a second mutable reference, preventing aliasing UB. - Lock-free ring buffer: the buffer shared between the calling thread and the drain thread uses atomic ordering to coordinate access without locks. The calling thread only writes; the drain thread only reads.
- Affinity syscalls: platform thread-affinity calls require raw pointer
and FFI usage, gated behind
#[cfg(target_os)].
Macros§
- debug
- Logs a message at
Level::Debug. - error
- Logs a message at
Level::Error. - info
- Logs a message at
Level::Info. - trace
- Logs a message at
Level::Trace. - warn
- Logs a message at
Level::Warn.
Structs§
- Builder
- Collects configuration for the logging system. Create one with
builder, then callBuilder::build. - Console
Sink - A
LogSinkthat writes to stdout or stderr, optionally coloring each line by its level. - FanOut
- A sink that dispatches to multiple inner sinks.
- File
Sink - A
LogSinkthat writes lines to a single file, buffered. - Guard
- A running logger. Keep it alive for as long as you want to log.
- With
Level - A
LogSinkadapter that overrides the maximum level of an inner sink. - Writer
Sink - A
LogSinkthat forwards each line, plus a trailing newline, to a wrappedio::Write.
Enums§
- Backpressure
- What a logging thread does when its buffer is full.
- Color
Mode - When
ConsoleSinkapplies ANSI colors. - Level
- The severity of a log message, ordered from most severe
(
Error) to least severe (Trace). - Ticklog
Error - Errors returned by ticklog operations.
Traits§
- LogSink
- A destination for formatted log lines.
- LogSink
Ext - Extension trait for
LogSinkproviding per-sink level filtering.
Functions§
- builder
- Creates a
Builderwith default configuration. - init
- Starts ticklog with a custom sink and default configuration. Equivalent to
builder().sink(sink).build(). - pin_
thread - Pin the calling thread to the given set of logical CPUs.
- warm_up
- Prepares the calling thread for logging by allocating its buffer up front, moving the one-time, first-log allocation off a latency-sensitive path.