宏 API(LOG_INFO)延迟最低,函数 API(quill::info)写法更自然但略慢;详细模式需自行调用 Backend::start() 并创建 sink,否则不会输出;指标需先用 Frontend::create_metric 注册元数据再发布样本

quill
odygrdMIT活跃维护面向性能敏感场景的超低延迟异步 C++17 日志与指标库,用异步写入避免日志拖慢主线程。
Ultra-low-latency asynchronous C++17 logging and metrics library for performance-critical applications
#include "quill/SimpleSetup.h"
#include "quill/LogMacros.h"
int main(){ auto* logger = quill::simple_logger(); LOG_INFO(logger, "Hello from {}!", "Quill"); }README 未说明
C++17 异步日志与指标库,把格式化与 IO 移出延迟敏感线程
面向性能敏感场景的超低延迟异步 C++17 日志与指标库,用异步写入避免日志拖慢主线程。
核心能力
- 前端编码日志参数入队,后端线程延迟格式化
- 同一异步后端发布预注册指标,内置 Prometheus sink
- 编译期裁剪日志级别、配置前端队列与内存策略
- 支持 rdtsc/chrono/自定义时钟与跨线程时间戳校正
- 提供环形缓冲回溯日志、JSON 输出与文件轮转 sink
容易踩的坑
- v13.1.0最新正式版2026-10-17 · 2026-10-17 11:17 UTC
- Added the opt-in
QUILL_IMPORT_STDoption for named module builds on toolchains supportingimport std; standard library headers remain the default. - Added the
--@quill//:std_moduleBazel flag to configure the standard library module target forquill_module, automatically enablingQUILL_IMPORT_STD. - CMake and Bazel module targets now define
QUILL_USE_MODULEautomatically for consumers, while retaining compatibility with an explicit#define QUILL_USE_MODULEin source code. - Uncaught exceptions from
Frontendlogger creation and sink lookup or creation no longer leave manager locks held, allowing crash handlers to look up loggers and flush pending logs. - Added direct logging of arithmetic
std::atomic<T>values viaquill/std/Atomic.h, capturing each value on the calling thread. - Added the experimental
quill_moduletarget for Bazel 9+ consumers using C++20 and--experimental_cpp_modules, while preserving ordinary header-only builds on older Bazel versions. - Fixed C++20 module imports using separate logger state across translation units, which could prevent logger lookup and crash automatic backend shutdown.
- Added
Logger::publish_dynamic_metric()andDYNAMIC_METRIC()for runtime labels, sharingPrometheusSinkseries with pointer publishing; existing registrations configure both APIs. - Added
PrometheusSink::register_*_family()to configure a metric family without creating a series. Useregister_*_family()when there is no initial metadata pointer to register. - CMake, Meson, and Bazel consumers can use a separate
fmtquilldependency for the bundled formatter without inheriting Quill's logging compile options, definitions, or link dependencies. - Fixed
FileSink::open_file()retaining the old filename when a subclass reopens a different path, which could redirect later logs back to the original file. FileSinknow accepts custom filename timestamp formats that expand to an empty string, such as%pin locales without AM/PM markers.StreamSinkcan retry signal-interrupted writes to streams configured with_IONBFby passingstream_is_unbuffered=trueto its constructor; short writes on buffered streams still report errors.- Loggers using
ClockSourceType::Systemno longer stall behind the timestamp-ordering grace check after a backward wall-clock correction. RotatingFileSinknow recognizes existing backup filename aliases during startup, preserving append-mode backups when the base filename spelling changes on a case- or normalization-insensitive filesystem.- On POSIX,
Frontend::create_or_get_sink<FileSink>()now resolves existing file aliases, preventing duplicate sinks from truncating or reordering output to the same log file. Backendnow retries thread discovery after a caught allocation failure, allowing a newly registered thread's pending logs to be processed.- Fixed
FileSinkrecovery after a deleted-file close callback throws, allowing later flushes to reopen the log. %(process_id)now contains the process ID when records are processed byManualBackendWorker.- With a dropping queue,
Logger::flush_log()now reports outstanding drops throughBackendOptions::error_notifiereven when the thread that dropped the messages has exited. Backend::stop()now processes records logged byFileEventNotifier::before_closeorFileEventNotifier::after_closewhile destroying removed loggers' sinks. This applies whenBackendOptions::wait_for_queues_to_empty_before_exitis enabled.- On Windows,
Frontend::create_or_get_sink<FileSink>()now reuses an existing sink for equivalent casing or\\?\spellings of the same path. In'w'mode, this prevents a second sink from reopening and truncating that file. - Fixed
FileSinkfailing to open long Windows paths supplied with a\\?\prefix. - On Windows, timestamp suffixes enabled by
FileSinkConfig::set_filename_append_option()now preserve Unicodefs::pathnames outside the acti
- Added the opt-in
- v13.0.02026-08-30 · 2026-08-30 10:33 UTC
查看更新说明
- Added
quill::log()to macro-free mode for logging with a runtime-selected log level. - Added backend tag processing through
PatternFormatterOptions::process_tagsand support for constructing macro-freequill::Tagsfromstd::stringandstd::string_view. (#970) - Added opt-in thread migration to
ManualBackendWorkerfor externally serialized executor and task-pool integrations. (#972) - Avoided allocating physical pages for unused bounded queue scratch space until it is needed. (#979)
- Precompiled
PatternFormatterfields when the formatter is created instead of parsing the pattern for every log record. - Added a backend fast path for applications with a single active frontend thread context.
- Added Android NDK compile coverage for API 24 on
arm64-v8aandx86_64. - Improvements to frontend throughput for macro-free logging and backend throughput for filtering, structured logging, and reusable transit-event data.
- Added
- v12.2.22026-08-24 · 2026-08-24 16:34 UTC
查看更新说明
- Fixed a benchmark build failure on x86 caused by a missing include.
- Fixed inaccurate hot-path benchmark timing on AArch64.
- v12.2.12026-08-24 · 2026-08-24 01:18 UTC
查看更新说明
- Fixed a frontend performance regression when logging variable-length arguments.
- Updated hot-path benchmark timing and producer pacing for more consistent measurements.
- Added Bazel 9 support by loading
cc_libraryfromrules_ccand updating the Bazel dependencies.
- v12.2.02026-08-23 · 2026-08-23 12:59 UTC
查看更新说明
- Added
SinkFlushReasonandSink::flush_sink(SinkFlushReason)so custom sinks can opt in to distinguishing periodic backend flushes, explicitlogger->flush_log()flushes, and final backend shutdown flushes. - Fixed the built-in signal handler suppressing core dumps for fatal signals. (#962)
- Improve frontend codegen.
- Added missing includes to quill module. (#960)
- Fixed indexed placeholders and JSON messages in interval rate-limited logs.
- Added overflow protection for backend duration settings and timestamp grace periods.
- Repeated backend error notifications are now rate-limited.
- Fixed duplicate flushes when multiple threads share a logger configured with
set_immediate_flush(N)whereN > 1. - Removed the
QUILL_X86ARCHbuild option along with the x86 cache-line flush and prefetch code it enabled. Benchmarks on Intel and AMD systems across several thread-pinning topologies showed no measurable benefit.
- Added
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
🧭 Table of Contents
✨ Introduction
Quill is an asynchronous logging and metrics library for C++17 and later. It keeps formatting and I/O away from latency-sensitive application threads by encoding log arguments on the frontend and processing them on a dedicated backend worker.
- Low Frontend Latency: Log arguments are encoded and queued for asynchronous processing, minimizing work on the calling thread. See the latency benchmarks for measured results and methodology.
- Deferred Formatting: Expensive formatting is performed by the backend worker instead of the calling thread.
- Logging and Metrics: Publish pre-registered metrics through the same asynchronous backend. The bundled Prometheus sink handles common metric types, while custom sinks can route samples to StatsD, OpenTelemetry, or other collectors. See the Metrics guide.
- Highly Customizable: Tune frontend queues and memory policy at compile time; configure backend idle behaviour, CPU affinity, buffering, timestamp handling, flushing, and callbacks at runtime; and compose loggers from built-in or custom sinks with per-sink filters. See Frontend Options, Backend Options, and Sinks.
- Production-Focused Testing: Continuously tested across Linux, macOS, Windows, and BSD, with sanitizers and fuzzing.
Using Quill? Click Star at the top of the GitHub repository to help other C++ developers discover it.
⏩ Quick Start
Getting started is easy and straightforward. Follow these steps to integrate the library into your project:
Installation
You can install Quill using the package manager of your choice:
| Package Manager | Installation Command |
|---|---|
| vcpkg | vcpkg install quill |
| Conan | conan install quill |
| Homebrew | brew install quill |
| Meson WrapDB | meson wrap install quill |
| Conda | conda install -c conda-forge quill |
| Bzlmod | bazel_dep(name = "quill", version = "x.y.z") |
| xmake | xrepo install quill |
| nix | nix-shell -p quill-log |
| build2 | libquill |
Setup
Quickest Setup
For the shortest path from zero to working logs, use simple_logger():
#include "quill/SimpleSetup.h"
#include "quill/LogMacros.h"
int main()
{
// log to the console
auto* logger = quill::simple_logger();
LOG_INFO(logger, "Hello from {}!", "Quill");
// log to a file
auto* logger2 = quill::simple_logger("test.log");
LOG_WARNING(logger2, "This message goes to a file");
}
Console output:
20:07:18.423476231 [48917] main.cpp:8 LOG_INFO Hello from Quill!
Detailed Setup
If you want explicit control over backend options, logger names, sinks, or formatters, use the
Backend and Frontend APIs directly:
#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
#include "quill/sinks/ConsoleSink.h"
#include <string_view>
int main()
{
quill::Backend::start();
quill::Logger* logger = quill::Frontend::create_or_get_logger(
"root", quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1"));
LOG_INFO(logger, "Hello from {}!", std::string_view{"Quill"});
}
Output:
20:07:18.423476231 [48917] main.cpp:15 LOG_INFO root Hello from Quill!
You can also use the macro-free mode. The macro API (LOG_INFO) is the lowest-latency path.
The function API (quill::info) reads more like ordinary code but is slightly slower.
See here for the trade-offs.
#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogFunctions.h"
#include "quill/Logger.h"
#include "quill/sinks/ConsoleSink.h"
#include <string_view>
int main()
{
quill::Backend::start();
quill::Logger* logger = quill::Frontend::create_or_get_logger(
"root", quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1"));
quill::info(logger, "Hello from {}!", std::string_view{"Quill"});
}
Publishing Metrics
Register MetricMetadata once, then publish double samples from hot threads through the same asynchronous backend
used for logs. The bundled PrometheusSink handles counters, gauges, histograms, and summaries; custom sinks can route
samples to StatsD, OpenTelemetry, or any in-process collector via Sink::write_metric().
// One-time registration — returns a stable pointer valid for program lifetime.
quill::MetricMetadata const* requests_total = quill::Frontend::create_metric(
"requests_total_post_200", "requests_total", {{"method", "POST"}, {"status", "200"}});
// Hot path — no label serialization, just a pointer and a double.
logger->publish_metric(requests_total, 1.0);
See the Metrics guide for sink setup, custom sinks, and Prometheus integration.
🎯 Features
- High-Performance: Ultra-low latency performance.
- Asynchronous Processing: Background thread handles formatting and I/O, keeping your main thread responsive.
- Metric Publishing: Publish pre-registered metric samples to Prometheus, StatsD, OpenTelemetry, or any in-process collector through the same asynchronous backend. See Metrics.
- Minimal Header Includes:
- Frontend: Only
Logger.handLogMacros.hneeded for logging. Lightweight with minimal dependencies. - Backend: Single
.cppfile inclusion. No backend code injection into other translation units.
- Frontend: Only
- Compile-Time Optimization: Eliminate specific log levels at compile time.
- Custom Formatters: Define your own log output patterns. See Formatters.
- Cross-Thread Timestamp Handling: The backend compares available events across frontend queues by timestamp, with a configurable grace window for delayed producers and optional sink-visible monotonic timestamp correction. See Timestamp Types.
- Flexible Timestamps: Support for
rdtsc,chrono, orcustom clocks- ideal for simulations and more. - Backtrace Logging: Store messages in a ring buffer for on-demand display. See Backtrace Logging
- Multiple Output Sinks: Console (with color), files (with rotation), JSON, ability to create custom sinks and more.
- Log Filtering: Process only relevant messages. See Filters.
- JSON Logging: Structured log output. See JSON Logging
- Mapped Diagnostic Context (MDC): Thread-local key/value context attached automatically to subsequent log lines. See MDC.
- Rate-Limited Macros:
LOG_*_LIMIT/LOGV_*_LIMITemit at most once per configured interval per call site. - Configurable Queue Modes:
bounded/unboundedandblocking/droppingoptions with monitoring on dropped messages, queue reallocations, and blocked hot threads. - Crash Handling: Built-in signal handler for log preservation during crashes.
- Huge Pages Support (Linux): Leverage huge pages on the hot path for optimized performance.
- Wide Character Support (Windows): Logs wide strings by converting them to UTF-8 on the backend, with support for STL containers consisting of wide strings.
- Exception-Free Option: Configurable builds with or without exception handling.
- Clean Codebase: Maintained to high standards, warning-free even at strict levels.
- Type-Safe API: Built on {fmt} library.
🚀 Performance
System Configuration
-
Quill Version: v13.0.0
-
OS: Linux RHEL 9.4
-
CPU: Intel Core i5-12600 (12th Gen) @ 4.8 GHz
-
Compiler: GCC 14.2
-
Build: Release with
-march=x86-64-v3 -
Benchmark-Tuned System: The system is specifically tuned for benchmarking.
-
Command Line Parameters:
$ cat /proc/cmdline BOOT_IMAGE=(hd0,gpt2)/vmlinuz-5.14.0-427.13.1.el9_4.x86_64 root=/dev/mapper/rhel-root ro crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M resume=/dev/mapper/rhel-swap rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap rhgb quiet nohz=on nohz_full=1-5 rcu_nocbs=1-5 isolcpus=1-5 mitigations=off transparent_hugepage=never intel_pstate=disable nosoftlockup irqaffinity=0 processor.max_cstate=1 nosoftirqd sched_tick_offload=0 spec_store_bypass_disable=off spectre_v2=off iommu=pt
You can find the benchmark code on the logger_benchmarks repository.
Latency
The results presented in the tables below are measured in nanoseconds (ns).
The tables are sorted by the 90th percentile (lower is better).
Logging Numbers
LOG_INFO(logger, "Logging int: {}, int: {}, double: {}", i, j, d).
1 Thread Logging
| Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
|---|---|---|---|---|---|---|
| fmtlog | 6 | 6 | 6 | 6 | 7 | 10 |
| Quill Bounded Dropping Queue | 6 | 6 | 6 | 6 | 8 | 9 |
| XTR | 6 | 6 | 6 | 6 | 9 | 10 |
| Quill Unbounded Queue | 6 | 6 | 6 | 7 | 8 | 10 |
| PlatformLab NanoLog | 8 | 8 | 9 | 10 | 10 | 11 |
| Quill - Macro Free Mode | 11 | 12 | 14 | 15 | 16 | 18 |
| MS BinLog | 18 | 18 | 18 | 19 | 73 | 119 |
| Reckless | 26 | 28 | 31 | 33 | 35 | 41 |
| BqLog | 125 | 133 | 138 | 141 | 151 | 190 |
| Iyengar NanoLog | 106 | 116 | 155 | 163 | 392 | 491 |
| spdlog | 271 | 280 | 296 | 309 | 337 | 360 |
| g3log | 1066 | 1081 | 1095 | 1103 | 1120 | 1143 |
| Boost.Log | 3093 | 3149 | 3259 | 3297 | 3464 | 3621 |
4 Threads Logging Simultaneously
| Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
|---|---|---|---|---|---|---|
| fmtlog | 8 | 8 | 8 | 8 | 10 | 14 |
| Quill Unbounded Queue | 8 | 8 | 8 | 8 | 11 | 17 |
| Quill Bounded Dropping Queue | 8 | 8 | 8 | 8 | 17 | 18 |
| XTR | 8 | 8 | 8 | 9 | 17 | 18 |
| PlatformLab NanoLog | 14 | 14 | 14 | 14 | 16 | 21 |
| Quill - Macro Free Mode | 13 | 14 | 17 | 21 | 25 | 32 |
| MS BinLog | 29 | 29 | 29 | 31 | 243 | 445 |
| Reckless | 27 | 37 | 44 | 47 | 59 | 96 |
| Iyengar NanoLog | 73 | 78 | 267 | 280 | 396 | 1419 |
| BqLog | 109 | 395 | 410 | 419 | 447 | 621 |
| spdlog | 557 | 585 | 614 | 640 | 741 | 1106 |
| g3log | 1188 | 1300 | 1402 | 1484 | 1631 | 1936 |
| Boost.Log | 1582 | 2644 | 3148 | 3177 | 3878 | 5050 |
Logging Large Strings
Logging std::string over 35 characters to prevent the short string optimization.
LOG_INFO(logger, "Logging int: {}, int: {}, string: {}", i, j, large_string).
1 Thread Logging
| Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
|---|---|---|---|---|---|---|
| fmtlog | 8 | 8 | 9 | 10 | 12 | 13 |
| XTR | 7 | 8 | 9 | 10 | 12 | 15 |
| PlatformLab NanoLog | 11 | 11 | 12 | 13 | 14 | 16 |
| Quill Bounded Dropping Queue | 9 | 10 | 12 | 13 | 15 | 19 |
| Quill Unbounded Queue | 11 | 12 | 14 | 16 | 18 | 21 |
| MS BinLog | 20 | 20 | 21 | 22 | 77 | 122 |
| Quill - Macro Free Mode | 17 | 19 | 21 | 23 | 25 | 28 |
| Reckless | 88 | 104 | 111 | 114 | 120 | 135 |
| BqLog | 125 | 132 | 137 | 141 | 156 | 191 |
| Iyengar NanoLog | 104 | 113 | 153 | 161 | 381 | 469 |
| spdlog | 247 | 253 | 260 | 266 | 278 | 290 |
| g3log | 838 | 848 | 856 | 861 | 870 | 892 |
| Boost.Log | 2844 | 2992 | 3019 | 3050 | 3140 | 3263 |
4 Threads Logging Simultaneously
| Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
|---|---|---|---|---|---|---|
| fmtlog | 8 | 9 | 10 | 13 | 16 | 23 |
| Quill Bounded Dropping Queue | 8 | 9 | 14 | 16 | 21 | 26 |
| XTR | 8 | 9 | 16 | 17 | 21 | 27 |
| PlatformLab NanoLog | 15 | 15 | 17 | 20 | 22 | 27 |
| Quill - Macro Free Mode | 10 | 11 | 18 | 24 | 28 | 35 |
| Quill Unbounded Queue | 17 | 18 | 19 | 22 | 27 | 29 |
| MS BinLog | 30 | 31 | 35 | 37 | 250 | 450 |
| Reckless | 44 | 85 | 132 | 144 | 165 | 183 |
| Iyengar NanoLog | 74 | 86 | 279 | 292 | 471 | 1457 |
| BqLog | 137 | 396 | 412 | 424 | 463 | 647 |
| spdlog | 529 | 558 | 588 | 613 | 695 | 1074 |
| g3log | 950 | 1029 | 1085 | 1202 | 1356 | 1577 |
| Boost.Log | 1322 | 2512 | 2923 | 3096 | 3737 | 4761 |
Logging Complex Types
Logging std::vector<std::string> containing 16 large strings, each ranging from 50 to 60 characters.
Note: some of the previous loggers do not support passing a std::vector as an argument.
LOG_INFO(logger, "Logging int: {}, int: {}, vector: {}", i, j, v).
1 Thread Logging
| Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
|---|---|---|---|---|---|---|
| Quill Bounded Dropping Queue | 53 | 58 | 63 | 69 | 99 | 120 |
| MS BinLog | 60 | 62 | 64 | 67 | 72 | 369 |
| Quill Unbounded Queue | 113 | 123 | 132 | 138 | 147 | 157 |
| XTR | 752 | 788 | 826 | 849 | 895 | 959 |
| fmtlog | 774 | 812 | 849 | 869 | 911 | 958 |
| Boost.Log | 4009 | 4083 | 4145 | 4250 | 4411 | 4705 |
| spdlog | 6906 | 7013 | 7120 | 7189 | 7447 | 8140 |
4 Threads Logging Simultaneously
| Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
|---|---|---|---|---|---|---|
| MS BinLog | 74 | 80 | 90 | 99 | 304 | 531 |
| Quill Bounded Dropping Queue | 67 | 75 | 93 | 104 | 117 | 130 |
| Quill Unbounded Queue | 78 | 89 | 103 | 112 | 129 | 147 |
| fmtlog | 674 | 701 | 726 | 740 | 770 | 808 |
| XTR | 674 | 713 | 752 | 776 | 816 | 849 |
| Boost.Log | 2624 | 3590 | 4342 | 4439 | 5457 | 7219 |
| spdlog | 7047 | 7284 | 7581 | 7943 | 8669 | 9711 |
Each latency observation is the average of 20 log calls made in a tight loop. The benchmark waits approximately 2 milliseconds between observations and repeats this process for the configured number of iterations.
For Quill Bounded Dropping Queue, the queue size is 262,144 bytes, twice the default size of 131,072 bytes.
Throughput
Throughput measures how many log messages the backend logging thread can write to a log file per second (higher is better). These tests use the same system configuration as the latency benchmarks.
The comparison is limited to asynchronous libraries with a flush-and-wait mechanism, ensuring that elapsed time covers processing every message. Binary-output modes are labelled and included in the table for reference, but are not directly comparable with human-readable text output, so the chart omits them.
Each benchmark logs 4 million instances of "Iteration: {} int: {} double: {}".
| Library | million msg/second | elapsed time |
|---|---|---|
| MS BinLog (binary log) | 61.79 | 64 ms |
| BqLog (binary log) | 12.86 | 311 ms |
| XTR | 7.73 | 517 ms |
| Quill | 6.44 | 620 ms |
| BqLog | 5.49 | 728 ms |
| Quill - Macro Free Mode | 5.13 | 779 ms |
| fmtlog | 2.69 | 1485 ms |
| Reckless | 2.58 | 1548 ms |
| spdlog | 2.57 | 1557 ms |
| Boost.Log | 0.33 | 12164 ms |
Compilation Time
Compile times are measured on the system above using clean Release builds of
BENCHMARK_quill_compile_time,
which compiles 2000 auto-generated log statements with varied argument types.
The measurements below were taken with -march=x86-64-v3 for Release, running one clean build
at a time with -j4.
Clang builds additionally enable -ftime-trace.
Quill intentionally keeps call-site metadata such as file, line, format string, and tags out of the
frontend template identity. In the common macro-based path, that information is stored in a
MacroMetadata object and passed as a regular function argument. As a result, multiple log statements
with the same argument type pack can reuse the same log_statement instantiation; changing only the
call-site metadata does not create a new frontend template instantiation.
| Compiler | Clean Build Time | Benchmark Binary | Main TU Object |
|---|---|---|---|
clang 17.0.6 | 30.64 s | 5.87 MB | 10.10 MB |
gcc 13.3.1 | 61.20 s | 6.22 MB | 9.28 MB |
Header include profile — shows the additional headers pulled in when logging, following the recommended_usage example:
Compile-time benchmark — measures compilation of 2000 auto-generated log statements with various arguments:
To generate these profiles yourself:
cmake -G Ninja -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release \
-DQUILL_BUILD_BENCHMARKS=ON -DQUILL_ENABLE_TIME_TRACE=ON \
-DCMAKE_CXX_FLAGS='-march=x86-64-v3' ..
cmake --build . --target BENCHMARK_quill_compile_time -j 4
# Load the resulting .cpp.json files into https://www.speedscope.app
Verdict
Quill combines very low frontend latency with competitive text-output throughput while retaining a broad feature set.
The human-readable log files facilitate easier debugging and analysis. While initially larger, they compress efficiently, with the size difference between human-readable and binary logs becoming minimal once zipped.
For example, for the same number of messages:
ms_binlog_backend_total_time.blog (binary log): 177 MB
ms_binlog_backend_total_time.zip (zipped binary log): 35 MB
quill_backend_total_time.log (human-readable log): 448 MB
quill_backend_total_time.zip (zipped human-readable log): 47 MB
If you prefer a binary-log workflow, MS BinLog is a strong alternative. It delivers excellent hot-path latency and smaller raw files, but it trades away immediate readability and requires offline processing tools.
🧩 Usage
Also, see the Quick Start Guide for a brief introduction.
#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
#include "quill/sinks/ConsoleSink.h"
#include "quill/std/Array.h"
#include <string>
#include <utility>
int main()
{
// Backend
quill::BackendOptions backend_options;
quill::Backend::start(backend_options);
// Frontend
auto console_sink = quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1");
quill::Logger* logger = quill::Frontend::create_or_get_logger("root", std::move(console_sink));
// Change the LogLevel to print everything
logger->set_log_level(quill::LogLevel::TraceL3);
// A log message with number 123
int a = 123;
std::string l = "log";
LOG_INFO(logger, "A {} message with number {}", l, a);
// libfmt formatting language is supported 3.14e+00
double pi = 3.141592653589793;
LOG_INFO(logger, "libfmt formatting language is supported {:.2e}", pi);
// Logging STD types is supported [1, 2, 3]
std::array<int, 3> arr = {1, 2, 3};
LOG_INFO(logger, "Logging STD types is supported {}", arr);
// Logging STD types is supported [arr: [1, 2, 3]]
LOGV_INFO(logger, "Logging STD types is supported", arr);
// A message with two variables [a: 123, b: 3.17]
double b = 3.17;
LOGV_INFO(logger, "A message with two variables", a, b);
for (uint32_t i = 0; i < 10; ++i)
{
// Will only log the message once per second
LOG_INFO_LIMIT(std::chrono::seconds{1}, logger, "A {} message with number {}", l, a);
LOGV_INFO_LIMIT(std::chrono::seconds{1}, logger, "A message with two variables", a, b);
}
LOG_TRACE_L3(logger, "Support for floats {:03.2f}", 1.23456);
LOG_TRACE_L2(logger, "Positional arguments are {1} {0} ", "too", "supported");
LOG_TRACE_L1(logger, "{:>30}", std::string_view {"right aligned"});
LOG_DEBUG(logger, "Debugging foo {}", 1234);
LOG_INFO(logger, "Welcome to Quill!");
LOG_WARNING(logger, "A warning message.");
LOG_ERROR(logger, "An error message. error code {}", 123);
LOG_CRITICAL(logger, "A critical error.");
}
Output
External CMake
Building and Installing Quill
To get started with Quill, clone the repository and install it using CMake:
git clone https://github.com/odygrd/quill.git
cd quill
mkdir cmake_build
cd cmake_build
cmake ..
make install
- Custom Installation: Specify a custom directory with
-DCMAKE_INSTALL_PREFIX=/path/to/install/dir. - Build Examples: Include examples with
-DQUILL_BUILD_EXAMPLES=ON.
Next, add Quill to your project using find_package():
find_package(quill REQUIRED)
target_link_libraries(your_target PUBLIC quill::quill)
Sample Directory Structure
Organize your project directory like this:
my_project/
├── CMakeLists.txt
├── main.cpp
Sample CMakeLists.txt
Here is a minimal CMakeLists.txt:
# If Quill is in a non-standard directory, specify its path.
set(CMAKE_PREFIX_PATH /path/to/quill)
# Find and link the Quill library.
find_package(quill REQUIRED)
add_executable(example main.cpp)
target_link_libraries(example PUBLIC quill::quill)
Embedded CMake
If you prefer to vendor Quill directly, add it as a subdirectory:
Sample Directory Structure
my_project/
├── quill/ # Quill repo folder
├── CMakeLists.txt
├── main.cpp
Sample CMakeLists.txt
Use this CMakeLists.txt to include Quill directly:
cmake_minimum_required(VERSION 3.8)
project(my_project)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(quill)
add_executable(my_project main.cpp)
target_link_libraries(my_project PUBLIC quill::quill)
Android NDK
Android usually works without special handling. If your toolchain does not support thread names, configure with:
-DQUILL_NO_THREAD_NAME_SUPPORT:BOOL=ON
For timestamps, use quill::ClockSourceType::System. Quill also includes an AndroidSink for
Android's logging system.
Minimal Example to Start Logging on Android
quill::Backend::start();
auto sink = quill::Frontend::create_or_get_sink<quill::AndroidSink>("app", [](){
quill::AndroidSinkConfig asc;
asc.set_tag("app");
asc.set_format_message(true);
return asc;
}());
auto logger = quill::Frontend::create_or_get_logger("root", std::move(sink),
quill::PatternFormatterOptions {},
quill::ClockSourceType::System);
LOG_INFO(logger, "Test {}", 123);
Meson
Using WrapDB
Install Quill from Meson's wrapdb with:
meson wrap install quill
Manual Integration
Or copy the repository into subprojects and add the following to meson.build:
quill = subproject('quill')
quill_dep = quill.get_variable('quill_dep')
my_build_target = executable('name', 'main.cpp', dependencies : [quill_dep], install : true)
Bazel
Using Bzlmod
Quill is available on Bzlmod.
Manual Integration
For manual setup, add Quill to your BUILD.bazel file like this:
cc_binary(name = "app", srcs = ["main.cpp"], deps = ["//quill_path:quill"])
📐 Design
Quill is split into a hot frontend and a cold backend.
- Each frontend thread owns a lock-free SPSC queue.
LOG_*macros binary-serialize arguments directly into that queue — no shared state, no contention between threads, no formatting work on the caller. - A single backend worker drains all queues, merges events in timestamp order, invokes the
per-argument-pack decode function to reconstruct arguments, runs
{fmt}formatting and thePatternFormatter, and writes the resulting log lines or metric samples to the attachedSinks.
Frontend (caller-thread)
When invoking a LOG_ macro:
-
Creates a static constexpr metadata object containing the format string and source location.
-
Pushes the event into the SPSC lock-free queue. For each log message, Quill enqueues:
| Variable | Description |
|---|---|
| timestamp | Current timestamp |
| Metadata* | Pointer to metadata information |
| Logger* | Pointer to the logger instance |
| DecodeFunc | A pointer to a templated function containing all the log message argument types, used for decoding the message |
| Args... | A serialized binary copy of each log message argument that was passed to the LOG_ macro |
When invoking METRIC(...) or logger->publish_metric():
-
Reuses pre-registered
MetricMetadata, so metric names and labels are not serialized again on the hot path. -
Pushes a compact fixed-size sample record to the same SPSC queue.
| Variable | Description |
|---|---|
| timestamp | Current timestamp |
| MetricMetadata* | Pointer to the pre-registered metric name and labels |
| Logger* | Pointer to the logger instance |
| value | The actual sample value as a double (counter delta, latency, gauge) |
Backend
The backend thread drains the SPSC queue, reconstructs log events, forwards metric samples to
Sink::write_metric(), and fans each log or metric event out to the sinks attached to the logger.
Architecture Overview
The diagram below shows the end-to-end flow from hot frontend threads to the backend worker and sinks.
🚨 Caveats
Do not log from destructors of static or global objects. Quill's internal singletons are function-local statics destroyed in reverse construction order. If a static object's constructor triggers the first log call, the library singletons are constructed after that object and destroyed before it. Logging from that destructor will then touch already-destroyed state.
Use fork() with care. Quill starts a background thread, and fork() interacts poorly with
multithreaded processes. If you need logging in child processes, call quill::Backend::start()
after fork() in each process that should log, and write parent and child output to different
files.
Example:
#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
#include "quill/sinks/FileSink.h"
int main()
{
// DO NOT CALL THIS BEFORE FORK
// quill::Backend::start();
if (fork() == 0)
{
quill::Backend::start();
// Write child output to its own file.
auto file_sink = quill::Frontend::create_or_get_sink<quill::FileSink>("child.log");
quill::Logger* logger = quill::Frontend::create_or_get_logger("root", std::move(file_sink));
LOG_INFO(logger, "Hello from Child {}", 123);
}
else
{
quill::Backend::start();
// Write parent output to its own file.
auto file_sink = quill::Frontend::create_or_get_sink<quill::FileSink>("parent.log");
quill::Logger* logger = quill::Frontend::create_or_get_logger("root", std::move(file_sink));
LOG_INFO(logger, "Hello from Parent {}", 123);
}
}
📝 License
Quill is licensed under the MIT License.
Quill depends on third party libraries with separate copyright notices and license terms. Your use of the source code for these subcomponents is subject to the terms and conditions of the following licenses.