ticklog
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.
Log calls run exclusively on the calling thread's hot path: check the level, encode a compact binary record into that thread's private lock-free buffer, and return. A background drain thread does the rest: decoding, formatting, timestamping, and writing each record, keeping all of that cost off the calling thread.
Features
- Nanosecond hot path: ~10 ns per call, over 20x faster than general-purpose loggers (see Benchmarks), with no per-call allocation, formatting, or I/O on the calling thread, so the cost stays small and predictable on latency-critical paths.
- Console/file sinks + Fanout: stdout or stderr (colored by level) and buffered files, with fan-out from one record to several sinks and per-sink level filtering.
- Ecosystem support: file rotation, compression, and retention come from existing crates like logroller and tracing-appender (see Examples); anything that implements
io::Writeplugs in as a sink. - Zero dependencies: no runtime dependencies; a minimal, self-contained crate.
Installation
[]
= "0.1"
Requires Rust 1.85 or newer (edition 2024).
Quick start
use ;
let _guard = builder
.sink
.build
.unwrap;
info!;
build() returns a Guard. Keep it alive for as long as you want to log: when it is dropped it flushes the sink, stops the background thread, and disables logging, so every log call afterwards is a silent no-op.
Benchmarks
Per-call latency on a Mac (M4, macOS 15, Rust 1.85, release profile). 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 |
Configuration
builder() returns a Builder with these options:
| Method | Purpose | Default |
|---|---|---|
sink(s) |
Where output goes. | ConsoleSink on stderr |
max_level(level) |
Records above this level are dropped on the calling thread before any encoding. | Level::Info |
backpressure(policy) |
What a logging thread does when its buffer is full. | Backpressure::Drop |
timezone_offset(seconds) |
Seconds east of UTC, applied to timestamp formatting only. | 0 (UTC) |
drain_affinity(&cores) |
Pin the background thread to a set of logical CPUs. | none |
Backpressure::Drop discards the record and returns immediately, never blocking the caller. Backpressure::Block spins until space frees up: it never drops records but burns CPU while the buffer stays full.
Sinks
A LogSink is the final destination for formatted lines. The crate ships three:
use ;
// stdout or stderr, colored by level (auto-detected, or forced on/off)
let console = stderr;
let plain = stdout.with_color;
// a buffered single file, appended to or truncated on open
let appended = new.unwrap;
let fresh = truncate.unwrap;
Compose and filter with FanOut (dispatch one record to several sinks) and with_max_level (limit a sink to a level and below):
use ;
let sink = new
.add
.add;
Custom sinks
For a destination that is not io::Write, such as a channel or a metrics counter, implement LogSink directly.
use io;
use UdpSocket;
use ;
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. pin_thread pins the calling thread to a set of logical CPUs.
// A latency-sensitive worker: pin it to a core and pre-allocate its buffer
// up front, so its first log call is as cheap as the rest.
let worker = spawn;
License
MIT