odygrd/quill

Ultra-low-latency asynchronous C++17 logging and metrics library for performance-critical applications

3,027

stars

1,700

commits

C++

primary language

Sep 10, 2026

updated

quillcpp.readthedocs.io
async
asynchronous
cpp
cpp17
cpp20
cpp-logging
cross-platform
fmtlib
high-performance
logger
logging
logging-library
log-library
low-latency
metrics
observability
prometheus
structured-logging

README


🧭 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 ManagerInstallation Command
vcpkgvcpkg install quill
Conanconan install quill
Homebrewbrew install quill
Meson WrapDBmeson wrap install quill
Condaconda install -c conda-forge quill
Bzlmodbazel_dep(name = "quill", version = "x.y.z")
xmakexrepo install quill
nixnix-shell -p quill-log
build2libquill

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.h and LogMacros.h needed for logging. Lightweight with minimal dependencies.
    • Backend: Single .cpp file inclusion. No backend code injection into other translation units.
  • 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, or custom 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_*_LIMIT emit at most once per configured interval per call site.
  • Configurable Queue Modes: bounded/unbounded and blocking/dropping options 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
Library50th75th90th95th99th99.9th
fmtlog6666710
Quill Bounded Dropping Queue666689
XTR6666910
Quill Unbounded Queue6667810
PlatformLab NanoLog889101011
Quill - Macro Free Mode111214151618
MS BinLog1818181973119
Reckless262831333541
BqLog125133138141151190
Iyengar NanoLog106116155163392491
spdlog271280296309337360
g3log106610811095110311201143
Boost.Log309331493259329734643621

Logging numbers 1-thread latency chart

4 Threads Logging Simultaneously
Library50th75th90th95th99th99.9th
fmtlog88881014
Quill Unbounded Queue88881117
Quill Bounded Dropping Queue88881718
XTR88891718
PlatformLab NanoLog141414141621
Quill - Macro Free Mode131417212532
MS BinLog29292931243445
Reckless273744475996
Iyengar NanoLog73782672803961419
BqLog109395410419447621
spdlog5575856146407411106
g3log118813001402148416311936
Boost.Log158226443148317738785050

Logging numbers 4-thread latency chart

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
Library50th75th90th95th99th99.9th
fmtlog889101213
XTR789101215
PlatformLab NanoLog111112131416
Quill Bounded Dropping Queue91012131519
Quill Unbounded Queue111214161821
MS BinLog2020212277122
Quill - Macro Free Mode171921232528
Reckless88104111114120135
BqLog125132137141156191
Iyengar NanoLog104113153161381469
spdlog247253260266278290
g3log838848856861870892
Boost.Log284429923019305031403263

Logging large strings 1-thread latency chart

4 Threads Logging Simultaneously
Library50th75th90th95th99th99.9th
fmtlog8910131623
Quill Bounded Dropping Queue8914162126
XTR8916172127
PlatformLab NanoLog151517202227
Quill - Macro Free Mode101118242835
Quill Unbounded Queue171819222729
MS BinLog30313537250450
Reckless4485132144165183
Iyengar NanoLog74862792924711457
BqLog137396412424463647
spdlog5295585886136951074
g3log95010291085120213561577
Boost.Log132225122923309637374761

Logging large strings 4-thread latency chart

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
Library50th75th90th95th99th99.9th
Quill Bounded Dropping Queue5358636999120
MS BinLog6062646772369
Quill Unbounded Queue113123132138147157
XTR752788826849895959
fmtlog774812849869911958
Boost.Log400940834145425044114705
spdlog690670137120718974478140

Logging complex types 1-thread latency chart

4 Threads Logging Simultaneously
Library50th75th90th95th99th99.9th
MS BinLog74809099304531
Quill Bounded Dropping Queue677593104117130
Quill Unbounded Queue7889103112129147
fmtlog674701726740770808
XTR674713752776816849
Boost.Log262435904342443954577219
spdlog704772847581794386699711

Logging complex types 4-thread latency chart

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: {}".

Librarymillion msg/secondelapsed time
MS BinLog (binary log)61.7964 ms
BqLog (binary log)12.86311 ms
XTR7.73517 ms
Quill6.44620 ms
BqLog5.49728 ms
Quill - Macro Free Mode5.13779 ms
fmtlog2.691485 ms
Reckless2.581548 ms
spdlog2.571557 ms
Boost.Log0.3312164 ms

Throughput comparison chart

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.

CompilerClean Build TimeBenchmark BinaryMain TU Object
clang 17.0.630.64 s5.87 MB10.10 MB
gcc 13.3.161.20 s6.22 MB9.28 MB

Header include profile β€” shows the additional headers pulled in when logging, following the recommended_usage example:

Open in Speedscope β†—

Compile-time benchmark β€” measures compilation of 2000 auto-generated log statements with various arguments:

Open in Speedscope β†—

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

example 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 the PatternFormatter, and writes the resulting log lines or metric samples to the attached Sinks.

Frontend (caller-thread)

When invoking a LOG_ macro:

  1. Creates a static constexpr metadata object containing the format string and source location.

  2. Pushes the event into the SPSC lock-free queue. For each log message, Quill enqueues:

VariableDescription
timestampCurrent timestamp
Metadata*Pointer to metadata information
Logger*Pointer to the logger instance
DecodeFuncA 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():

  1. Reuses pre-registered MetricMetadata, so metric names and labels are not serialized again on the hot path.

  2. Pushes a compact fixed-size sample record to the same SPSC queue.

VariableDescription
timestampCurrent timestamp
MetricMetadata*Pointer to the pre-registered metric name and labels
Logger*Pointer to the logger instance
valueThe 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.

design diagram


🚨 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.

Contributors

(top 30 of 46)

odygrd

1,516 commits

apozzer305

61 commits

HyunjinX917

30 commits

NanKendrik

17 commits

odygrd/quill

Ultra-low-latency asynchronous C++17 logging and metrics library for performance-critical applications

3,027

stars

1,700

commits

C++

primary language

Sep 10, 2026

updated

quillcpp.readthedocs.io
async
asynchronous
cpp
cpp17
cpp20
cpp-logging
cross-platform
fmtlib
high-performance
logger
logging
logging-library
log-library
low-latency
metrics
observability
prometheus
structured-logging

README


🧭 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 ManagerInstallation Command
vcpkgvcpkg install quill
Conanconan install quill
Homebrewbrew install quill
Meson WrapDBmeson wrap install quill
Condaconda install -c conda-forge quill
Bzlmodbazel_dep(name = "quill", version = "x.y.z")
xmakexrepo install quill
nixnix-shell -p quill-log
build2libquill

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.h and LogMacros.h needed for logging. Lightweight with minimal dependencies.
    • Backend: Single .cpp file inclusion. No backend code injection into other translation units.
  • 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, or custom 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_*_LIMIT emit at most once per configured interval per call site.
  • Configurable Queue Modes: bounded/unbounded and blocking/dropping options 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
Library50th75th90th95th99th99.9th
fmtlog6666710
Quill Bounded Dropping Queue666689
XTR6666910
Quill Unbounded Queue6667810
PlatformLab NanoLog889101011
Quill - Macro Free Mode111214151618
MS BinLog1818181973119
Reckless262831333541
BqLog125133138141151190
Iyengar NanoLog106116155163392491
spdlog271280296309337360
g3log106610811095110311201143
Boost.Log309331493259329734643621

Logging numbers 1-thread latency chart

4 Threads Logging Simultaneously
Library50th75th90th95th99th99.9th
fmtlog88881014
Quill Unbounded Queue88881117
Quill Bounded Dropping Queue88881718
XTR88891718
PlatformLab NanoLog141414141621
Quill - Macro Free Mode131417212532
MS BinLog29292931243445
Reckless273744475996
Iyengar NanoLog73782672803961419
BqLog109395410419447621
spdlog5575856146407411106
g3log118813001402148416311936
Boost.Log158226443148317738785050

Logging numbers 4-thread latency chart

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
Library50th75th90th95th99th99.9th
fmtlog889101213
XTR789101215
PlatformLab NanoLog111112131416
Quill Bounded Dropping Queue91012131519
Quill Unbounded Queue111214161821
MS BinLog2020212277122
Quill - Macro Free Mode171921232528
Reckless88104111114120135
BqLog125132137141156191
Iyengar NanoLog104113153161381469
spdlog247253260266278290
g3log838848856861870892
Boost.Log284429923019305031403263

Logging large strings 1-thread latency chart

4 Threads Logging Simultaneously
Library50th75th90th95th99th99.9th
fmtlog8910131623
Quill Bounded Dropping Queue8914162126
XTR8916172127
PlatformLab NanoLog151517202227
Quill - Macro Free Mode101118242835
Quill Unbounded Queue171819222729
MS BinLog30313537250450
Reckless4485132144165183
Iyengar NanoLog74862792924711457
BqLog137396412424463647
spdlog5295585886136951074
g3log95010291085120213561577
Boost.Log132225122923309637374761

Logging large strings 4-thread latency chart

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
Library50th75th90th95th99th99.9th
Quill Bounded Dropping Queue5358636999120
MS BinLog6062646772369
Quill Unbounded Queue113123132138147157
XTR752788826849895959
fmtlog774812849869911958
Boost.Log400940834145425044114705
spdlog690670137120718974478140

Logging complex types 1-thread latency chart

4 Threads Logging Simultaneously
Library50th75th90th95th99th99.9th
MS BinLog74809099304531
Quill Bounded Dropping Queue677593104117130
Quill Unbounded Queue7889103112129147
fmtlog674701726740770808
XTR674713752776816849
Boost.Log262435904342443954577219
spdlog704772847581794386699711

Logging complex types 4-thread latency chart

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: {}".

Librarymillion msg/secondelapsed time
MS BinLog (binary log)61.7964 ms
BqLog (binary log)12.86311 ms
XTR7.73517 ms
Quill6.44620 ms
BqLog5.49728 ms
Quill - Macro Free Mode5.13779 ms
fmtlog2.691485 ms
Reckless2.581548 ms
spdlog2.571557 ms
Boost.Log0.3312164 ms

Throughput comparison chart

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.

CompilerClean Build TimeBenchmark BinaryMain TU Object
clang 17.0.630.64 s5.87 MB10.10 MB
gcc 13.3.161.20 s6.22 MB9.28 MB

Header include profile β€” shows the additional headers pulled in when logging, following the recommended_usage example:

Open in Speedscope β†—

Compile-time benchmark β€” measures compilation of 2000 auto-generated log statements with various arguments:

Open in Speedscope β†—

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

example 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 the PatternFormatter, and writes the resulting log lines or metric samples to the attached Sinks.

Frontend (caller-thread)

When invoking a LOG_ macro:

  1. Creates a static constexpr metadata object containing the format string and source location.

  2. Pushes the event into the SPSC lock-free queue. For each log message, Quill enqueues:

VariableDescription
timestampCurrent timestamp
Metadata*Pointer to metadata information
Logger*Pointer to the logger instance
DecodeFuncA 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():

  1. Reuses pre-registered MetricMetadata, so metric names and labels are not serialized again on the hot path.

  2. Pushes a compact fixed-size sample record to the same SPSC queue.

VariableDescription
timestampCurrent timestamp
MetricMetadata*Pointer to the pre-registered metric name and labels
Logger*Pointer to the logger instance
valueThe 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.

design diagram


🚨 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.

Contributors

(top 30 of 46)

odygrd

1,516 commits

apozzer305

61 commits

HyunjinX917

30 commits

NanKendrik

17 commits

Languages

C++

96.0%

Python

2.0%

CMake

1.9%