Skip to main content

Crate ticklog

Crate ticklog 

Source
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 (p50, criterion). Lower is better.

NameLog call
one_u64info!("x={}", 42u64)
one_strinfo!("{}", "hello world")
mixedinfo!("{} {} {}", 42u64, 3.14159, "hello world")

Mac M4 (Apple M4, 4.4 GHz, macOS 15):

Loggerone_u64one_strmixed
ticklog5.2 ns5.9 ns7.0 ns
env_logger231 ns232 ns307 ns
slog274 ns269 ns454 ns
tracing386 ns425 ns458 ns

Granite Rapids (Intel Xeon 6982P-C, 3.9 GHz, Ubuntu 24.04):

Loggerone_u64one_strmixed
ticklog8.6 ns8.4 ns9.9 ns
env_logger370 ns371 ns491 ns
slog499 ns453 ns686 ns
tracing837 ns854 ns937 ns

§Quick start

use ticklog::{info, FileSink};

// Configure ticklog and keep the returned guard alive for as long as you
// want to log.
let _guard = ticklog::configure! {
    sink: FileSink::new("app.log").unwrap(),
}
.unwrap();

info!("listening on {}", 8080);

configure! 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 marks every ring dead so subsequent log calls become silent no-ops.

§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

configure! accepts these keys, each optional:

  • sink: where output goes. Defaults to a ConsoleSink on 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 to Level::Info.
  • backpressure: what a logging thread does when its buffer is full, either Backpressure::Drop (the default, never blocks) or Backpressure::Block (spin until space frees up).
  • timezone_offset: offset applied when formatting timestamps. Defaults to UTC (0).
  • drain_affinity: pin the drain thread to a set of logical CPUs (Option<Vec<usize>>). Defaults to None.

§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 (see ColorMode).
  • FileSink: a buffered single file, opened in append or truncate mode.
  • WriterSink: wraps any std::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 UnsafeCell guarded 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§

configure
Initializes the logging system and returns a Guard.
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§

ConsoleSink
A LogSink that writes to stdout or stderr, optionally coloring each line by its level.
FanOut
A sink that dispatches to multiple inner sinks.
FileSink
A LogSink that writes lines to a single file, buffered.
Guard
A running logger. Keep it alive for as long as you want to log.
WithLevel
A LogSink adapter that overrides the maximum level of an inner sink.
WriterSink
A LogSink that forwards each line, plus a trailing newline, to a wrapped io::Write.

Enums§

Backpressure
What a logging thread does when its buffer is full.
ColorMode
When ConsoleSink applies ANSI colors.
Level
The severity of a log message, ordered from most severe (Error) to least severe (Trace).
TicklogError
Errors returned by ticklog operations.

Traits§

LogSink
A destination for formatted log lines.
LogSinkExt
Extension trait for LogSink providing per-sink level filtering.

Functions§

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.