Skip to content

Repository files navigation

cLog++

Build StatusLicense: MITGitHub starsStandardPlatform

Zero-dependency, header-only structured logging for modern C++.

Async by default. Chainable API. JSON structured output. No macros. cLog++ is a small, single-namespace logger for developers who want structured, thread-safe logging without pulling in external dependencies or a heavy build.


Why cLog++?

  • Zero dependencies: no nlohmann/json, no Boost, no external runtime. Drop include/ into your project.
  • Header-only: include the headers and compile; nothing to link beyond the standard library and threads.
  • Structured JSON: every line carries a UTC ts, a level, an event, and your fields. Numbers and booleans are emitted as native JSON values, ready for ELK, Splunk, or jq.
  • Chainable API:log.info("user.login").kv("id", 42).kv("ok", true); — no macros.
  • Lossless async: the background worker drains a bounded queue; when the queue is full the producer waits (backpressure) instead of dropping logs.
  • Thread-safe: multiple threads may log on the same Logger concurrently; each log statement builds its own record.

Quick Start

1. Integrate

Copy the include/ directory into your project, or add cLog++ with CMake:

add_subdirectory(cLogpp) # or FetchContenttarget_link_libraries(your_appPRIVATEclogpp::clogpp)

2. Use

#include"logger.hpp"intmain() {
c_log::Logger log; // async by default; console sink writes to stderr
log.info("server.start")
.kv("port", 8080)
.kv("env", "production")
.kv("workers", 4);
log.error("db.connection_failed")
.kv("code", 503)
.kv("retrying", true);
// The Logger flushes and joins its worker when it goes out of scope.
}

Output (one JSON object per line):

{"ts":"2026-06-08T21:04:05.123Z","level":"info","event":"server.start","port":8080,"env":"production","workers":4}
{"ts":"2026-06-08T21:04:05.123Z","level":"error","event":"db.connection_failed","code":503,"retrying":true}

3. Build (without CMake)

g++ -std=c++17 -O2 -I./include main.cpp -o app -pthread
./app

Important

Keep the Logger alive for as long as you log through it. It owns a background worker thread and flushes all pending entries when it is destroyed. Do not let a LogRecord (the object returned by log.info(...)) outlive its Logger.


Output format

Each entry is a single-line JSON object with a fixed prefix followed by your fields:

FieldTypeNotes
tsstringISO-8601 UTC, millisecond precision
levelstringtrace / debug / info / warning / error / critical
eventstringthe event name you passed
your keysmixedstrings are quoted+escaped; ints, floats and bools are native JSON

Benchmarks

The repository includes a micro-benchmark (benchmarks/benchmark_logger.cpp). It times 100,000 end-to-end log calls including the full async drain, so the async numbers reflect real delivery (async mode is lossless), not dropped entries.

Measured on the author's machine (AMD Ryzen 7 9800X3D, Windows 11, g++ 15.2 -O3, single producer thread):

SinkModeTime per log (μs)
null (discard)sync~0.19
filesync~0.33
null (discard)async~0.47
fileasync~0.60

cLog++ benchmark results

Note

These are single-producer, end-to-end numbers. Async mode is not about higher total throughput here — the single background worker does all the serialization and I/O while the producer waits, so end-to-end it costs a little more than sync. What async buys you is keeping serialization and I/O off your calling thread (lower, more predictable hot-path latency) while guaranteeing no log is dropped. For raw end-to-end throughput on one thread, sync is simplest and fastest. Run the benchmark on your own hardware — your numbers will differ.

To reproduce:

cmake -S . -B build -DCLOGPP_BUILD_BENCHMARKS=ON && cmake --build build
./build/benchmark_logger # writes benchmark_results.csv
python benchmarks/plot_benchmarks.py # writes benchmark.png (needs matplotlib)

Features

  • Async & sync modes: choose with Logger::Mode. Async offloads work to a background worker; sync emits inline.
  • Lossless async: a bounded queue with backpressure — full queue blocks the producer instead of dropping entries. Capacity is configurable: Logger(Mode::Async, capacity).
  • Level filtering:set_level(Level::Warning) skips everything below warning; Level::None silences all output.
  • Native-typed fields:kv overloads for std::string, const char*, bool, integers (signed/unsigned, up to 64-bit), and double.
  • Sinks: built-in ConsoleSink (stderr), FileSink, and size-based RotatingFileSink. Add your own by implementing c_log::Sink.
  • Explicit flush:log.flush() blocks until everything submitted so far has been written, then flushes the sinks.

Threading model

cLog++ is safe to use from multiple threads:

  • Several threads may call log.info(...).kv(...) on the sameLogger at the same time. Each statement builds its own LogRecord, so there is no shared per-entry state to corrupt.
  • The async queue is guarded by a mutex (it is a multi-producer / single-consumer queue), and sink writes are serialized, so entries are never interleaved or torn.

The bundled spsc_ring_buffer.hpp is a lock-free single-producer/single-consumer primitive. The Logger wraps it in a mutex to make it multi-producer-safe, so the async path is mutex-synchronized, not lock-free.


Advanced Usage

Custom sinks

Route logs anywhere (network, database, custom format) by implementing Sink:

structNetworkSink : c_log::Sink {
voidlog(const std::string& msg) override { /* send msg to a remote server */ }
voidflush() override { /* optional: flush buffered output */ }
};
log.add_sink(std::make_unique<NetworkSink>());

Use log.clear_sinks() first if you want to drop the default console sink.

Filtering levels
log.set_level(c_log::Level::Warning); // ignore trace/debug/info
log.warn("system.low_memory"); // logged
log.info("system.heartbeat"); // ignored
Rotating file sink
#include"rotating_file_sink.hpp"// Rotate at ~10 MB, keep 5 backups: app.log -> app.log.1 -> ... -> app.log.5
log.clear_sinks();
log.add_sink(std::make_unique<c_log::RotatingFileSink>("app.log", 10 * 1024 * 1024, 5));
Flushing before a checkpoint
log.error("payment.failed").kv("order", 1234);
log.flush(); // block until written, e.g. before exiting or after a critical event

Contributing

Contributions are welcome — bug reports, features, and code.

  1. Check the Issues.
  2. Read the Contributing Guide.
  3. Open a Pull Request.

Roadmap:

  • Lossless async log draining (backpressure)
  • Zero-dependency JSON serializer with timestamp + level + native types
  • Console, file, and rotating-file sinks
  • Working level filtering
  • CMake build and CI tests on Linux, macOS, and Windows
  • Per-sink level thresholds
  • Optional source-location fields (file/line/function)

License

MIT © cLog++ Contributors.


If you find cLog++ useful, consider starring the repository on GitHub.

Note on AI involvement

Some portions of this project were implemented with the aid of AI tooling. As a result, some aspects of the code and design may differ from conventionally developed open-source projects. Reviews, suggestions, and contributions are very welcome.

About

Modern C++ structured async logger. No macros. Extensible. JSON output. Async-by-default.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages