logger
July 8, 2026 · View on GitHub
Deferred, lock-free logging built on the Kronuz scheduler and stash wheel. Extracted from Xapiand, where it is the original reason the wheel exists.
What it is
Many threads arm log lines without ever blocking each other; a single thread drains them and writes them in order; and a scheduled "this is taking too long" line can be cancelled with one atomic when the operation finishes in time, so the common case logs nothing. That last part is the point: it is built for log slow ops only, and for any pattern where you arm a timed line and usually cancel it.
A line takes one of three paths (see ARCHITECTURE.md): the severe levels write inline on the calling thread; routine levels are handed to the LOG thread keyed at now; deferred lines are keyed at now + delay and fire only if not cancelled first.
When to use it / when not
Use it when logging is on a hot path with many threads, when you want slow-path or debounced lines that you arm and usually cancel, and when one thread writing ordered output is what you want. It depends only on the scheduler.
Reach for something simpler if you have a single thread, or no notion of when,
or you need pluggable structured-logging sinks and severity routing beyond what
Logger handlers give you. This is a fast, lock-free arm with a deferred-cancel
trick, not a full logging framework.
Install
Header + one source file, consumed via CMake FetchContent:
include(FetchContent)
FetchContent_Declare(logger
GIT_REPOSITORY https://github.com/Kronuz/logger.git
GIT_TAG main)
FetchContent_MakeAvailable(logger)
target_link_libraries(your_target PRIVATE logger::logger)
That transitively pulls scheduler (and its threadpool + stash) and
term-color, whose stacked palette the sink resolves to the terminal's depth per
line (the collapse / apply color policy). Everything host-specific stays
out: exception description, real backtraces, thread names, and the timestamp
format come through LogHooks, so the core needs nothing else.
Usage
#include "logger.h"
int main() {
Logging::config.log_level = LOG_INFO; // emit INFO and more severe
Logging::config.color = LogColorMode::automatic; // color on a tty, honors NO_COLOR
Logging::add_handler(std::make_unique<StderrLogger>());
L_INFO("starting up, {} workers", 8);
L_WARNING("disk at {}%", 91);
{
// Arm a "too slow" line 200ms out; cancel it if we finish in time.
L_DELAYED_200("request {} is slow", req_id);
handle_request(req_id);
L_DELAYED_N_CLEAR(); // finished in time -> the line never prints
}
try { risky(); }
catch (...) { L_EXC("risky() failed"); } // logs the in-flight exception
Logging::finish(); // drain and stop the LOG thread before exit
}
The bundled examples/demo.cc is a runnable tour, built by a
top-level CMake build:
cmake -B build && cmake --build build && ./build/logger_demo
It tours the severity palette (each level's rgb marker, brightest = most severe),
the grey-gradient timestamp, std::format messages, exception logging, once-dedup,
stacked indentation, the deferred "log slow ops only" pattern, and the iTerm2
integration. Colorized on a tty (the demo forces it on); abbreviated here:
█ 2026-06-29 08:55:43.557 L_EMERG system is unusable
▉ 2026-06-29 08:55:43.557 L_ALERT action must be taken immediately
▊ 2026-06-29 08:55:43.557 L_CRIT critical condition
▋ 2026-06-29 08:55:43.557 L_ERR error condition
▌ 2026-06-29 08:55:43.557 L_WARNING warning condition
▍ 2026-06-29 08:55:43.557 L_NOTICE normal but significant
▎ 2026-06-29 08:55:43.558 L_INFO informational
▊ 2026-06-29 08:55:43.558 caught while doing work: something broke
▎ 2026-06-29 08:55:43.558 item A
▌ 2026-06-29 08:55:43.583 slow_op is taking too long ~201.7ms
Note what is not there: fast_op's "too long" line, because it finished in time
and cancelled it (slow_op's fired, annotated with how long it ran), and the
second identical once-line, which was deduped.
API reference
LogConfig (Logging::config)
log_level (emit when priority <= log_level), color (LogColorMode:
automatic / always / never, mirroring a --color flag), color_depth
(LogColorDepth: automatic / ansi16 / ansi256 / truecolor / stacked,
the tier term-color's stacked escapes are collapsed to), with_timestamp plus
timestamp (LogTimestamp), precision (LogPrecision), and
timestamp_gradient, with_threads, with_location, iterm2 (emit iTerm2 marks
/ tab tint / badge when stderr is an iTerm2 terminal), markers (the per-priority
severity bar), and max_pending (backpressure: 0 = unbounded, otherwise routine
async lines are dropped past it with a coalesced summary).
LogHooks (Logging::hooks)
Optional, each with a std default: describe_exception, backtrace,
thread_name, timestamp. A host injects richer versions (e.g. real backtraces,
named threads) without the core depending on them.
Sinks (Logger)
StreamLogger (a file), StderrLogger (tty-aware), SysLog. Install with
Logging::add_handler(...); every line fans out to all of them.
Macros
L_INFO / L_NOTICE / L_WARNING / L_ERR / L_CRIT / L_ALERT / L_EMERG,
L_EXC (the in-flight exception), the *_ONCE and *_ONCE_PER_MINUTE variants,
L_DELAYED_* with L_DELAYED_N_UNLOG / L_DELAYED_N_CLEAR, L_TIMED,
L_STACKED (nested indent), L_PRINT (undecorated), and L_DEBUG (compiled out
unless LOGGER_DEBUG). A line below the level evaluates none of its arguments.
The Log handle
Returned by the deferred macros. clear() cancels the pending line; unlog()
swaps its message; age() reports how long it lived. Dropping the handle runs
the cancel/swap.
License
MIT. See LICENSE.