Skip to content

odygrd/quill

Repository files navigation


🧭 Table of Contents


✨ Introduction

Quill is a high-performance asynchronous logging library written in C++. It is designed for low-latency, performance-critical applications where every microsecond counts.

  • Performance-Focused: Quill consistently outperforms many popular logging libraries.
  • Feature-Rich: Packed with advanced features to meet diverse logging needs.
  • Battle-Tested: Proven in demanding production environments. Extensively tested with sanitizers (ASan, UBSan, LSan) and fuzzed across a wide range of inputs.
  • Extensive Documentation: Comprehensive guides and examples available.
  • Community-Driven: Open to contributions, feedback, and feature requests.

Try it on Compiler Explorer


⏩ 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 quickest and simplest setup use simple_logger():

#include "quill/SimpleSetup.h"
#include "quill/LogFunctions.h"

int main()
{
  // log to the console
  auto* logger = quill::simple_logger();
  quill::info(logger, "Hello from {}!", "Quill");

  // log to a file
  auto* logger2 = quill::simple_logger("test.log");
  quill::warning(logger2, "This message goes to a file");
}

Detailed Setup

For more detailed control and configuration options, use the Backend and Frontend APIs:

#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"});
}

Alternatively, you can use the macro-free mode. See here for details on performance 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"});
}

🎯 Features

  • High-Performance: Ultra-low latency performance. View Benchmarks
  • Asynchronous Processing: Background thread handles formatting and I/O, keeping your main thread responsive.
  • 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.
  • Timestamp-Ordered Logs: Simplify debugging of multithreaded applications with chronologically ordered logs.
  • 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
  • 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): Compatible with ASCII-encoded wide strings and 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

  • OS: Linux RHEL 9.4

  • CPU: Intel Core i5-12600 (12th Gen) @ 4.8 GHz

  • Compiler: GCC 13.1

  • 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 95th 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
Quill Bounded Dropping Queue 8 8 9 9 10 12
Quill Unbounded Queue 8 8 9 9 11 13
fmtlog 8 9 9 10 12 13
PlatformLab NanoLog 13 14 16 18 21 24
MS BinLog 21 21 21 22 59 93
Quill Unbounded Queue (Log Functions) 27 28 29 30 31 33
XTR 7 7 29 31 33 55
Reckless 26 28 31 32 34 40
BqLog 42 42 43 75 83 94
Iyengar NanoLog 87 98 122 129 209 381
spdlog 145 148 151 154 162 171
g3log 775 781 787 791 805 967
Boost.Log 838 845 853 859 897 959

numbers_1_thread_logging.webp

4 Threads Logging Simultaneously
Library 50th 75th 90th 95th 99th 99.9th
Quill Unbounded Queue 8 9 9 9 12 15
fmtlog 8 9 9 10 12 13
XTR 7 8 9 10 32 39
Quill Bounded Dropping Queue 8 9 10 11 13 15
PlatformLab NanoLog 9 13 15 17 22 26
MS BinLog 21 22 22 23 61 98
Reckless 19 23 26 27 31 57
Quill Unbounded Queue (Log Functions) 28 29 30 31 33 44
BqLog 47 49 50 122 143 4248
Iyengar NanoLog 58 90 124 132 178 1478
spdlog 214 260 327 359 452 754
Boost.Log 1640 2538 2793 2896 4150 4962
g3log 1336 4102 4501 4641 6079 7363

numbers_4_thread_logging.webp

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 10 12 13 13 15 17
Quill Unbounded Queue 11 12 14 14 16 18
Quill Bounded Dropping Queue 12 13 14 15 16 19
MS BinLog 23 23 24 25 62 96
XTR 8 8 28 29 33 52
PlatformLab NanoLog 15 19 29 32 37 43
Quill Unbounded Queue (Log Functions) 31 33 34 35 36 38
BqLog 44 45 46 76 85 93
Reckless 89 105 111 114 120 131
Iyengar NanoLog 89 100 123 132 205 367
spdlog 129 133 137 141 149 157
g3log 565 568 571 573 582 740
Boost.Log 638 641 645 647 651 659

large_strings_1_thread_logging.webp

4 Threads Logging Simultaneously
Library 50th 75th 90th 95th 99th 99.9th
fmtlog 10 12 13 13 16 19
XTR 8 11 13 15 30 40
Quill Unbounded Queue 13 14 15 16 19 21
Quill Bounded Dropping Queue 13 15 16 17 20 22
PlatformLab NanoLog 11 16 20 24 38 46
MS BinLog 23 24 25 26 65 104
Quill Unbounded Queue (Log Functions) 31 33 35 36 39 46
Reckless 77 92 101 105 112 126
BqLog 51 55 59 119 136 2973
Iyengar NanoLog 55 86 103 128 170 1364
spdlog 196 227 281 310 395 690
Boost.Log 1357 2420 2607 2651 3939 5682
g3log 1050 3886 4271 4442 5693 6831

large_strings_4_thread_logging.webp

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 46 49 52 55 59 65
MS BinLog 67 69 71 73 79 279
Quill Unbounded Queue 139 150 160 165 173 183
XTR 286 297 341 346 353 589
fmtlog 646 660 685 705 733 767
spdlog 6301 6363 6419 6455 6802 7414
Boost.Log 34432 34623 34797 34928 35703 36233

vector_1_thread_logging.webp

4 Threads Logging Simultaneously
Library 50th 75th 90th 95th 99th 99.9th
Quill Bounded Dropping Queue 49 53 57 60 65 79
MS BinLog 69 72 74 76 82 292
Quill Unbounded Queue 84 91 98 103 113 130
fmtlog 675 701 740 756 779 809
XTR 627 1225 1317 1376 205300 287631
spdlog 6593 6671 6751 6815 7610 8794
Boost.Log 36322 80294 151775 177681 272649 362401

vector_4_thread_logging.webp

The benchmark methodology involves logging 20 messages in a loop, calculating and storing the average latency for those 20 messages, then waiting around ~2 milliseconds, and repeating this process for a specified number of iterations.

In the Quill Bounded Dropping benchmarks, the dropping queue size is set to 262,144 bytes, which is double the default size of 131,072 bytes.

Throughput

Throughput is measured by calculating the maximum number of log messages the backend logging thread can write to a log file per second (higher is better).

The tests were run on the same system used for the latency benchmarks.

Although Quill’s primary focus is not on maximizing throughput, it efficiently manages log messages across multiple threads. Benchmarking throughput of asynchronous logging libraries presents certain challenges. Some libraries may drop log messages, leading to smaller-than-expected log files, while others only provide asynchronous flushing, making it difficult to verify when the backend thread has fully processed all messages.

For comparison, we benchmark against other asynchronous logging libraries that offer guaranteed logging with a flush-and-wait mechanism.

Note that MS BinLog writes log data to a binary file, which requires offline formatting with an additional program—this makes it an unfair comparison, but it is included for reference.

Similarly, BqLog (binary log) uses the compressed binary log appender, and its log files are not human-readable unless processed offline. However, it is included for reference. The other version of BqLog is using a text appender and produces human-readable log files.

In the same way, Platformlab Nanolog also outputs binary logs and is expected to deliver high throughput. However, for reasons unexplained, the benchmark runs significantly slower (10x longer) than the other libraries, so it is excluded from the table.

Logging 4 million times the message "Iteration: {} int: {} double: {}"

Library million msg/second elapsed time
MS BinLog (binary log) 62.93 62 ms
BqLog (binary log) 14.71 271 ms
XTR 8.16 490 ms
Quill 5.24 763 ms
spdlog 4.32 925 ms
fmtlog 2.82 1417 ms
Reckless 2.72 1471 ms
Quill - Macro Free Mode 2.65 1510 ms
BqLog 2.60 1537 ms
Boost.Log 0.39 10102 ms

throughput.webp

Compilation Time

Compile times are measured using clang 17 and for Release build.

Below, you can find the additional headers that the library will include when you need to log, following the recommended_usage example

quill_v10_0_compiler_profile.speedscope.png

There is also a compile-time benchmark measuring the compilation time of 2000 auto-generated log statements with various arguments. You can find it here. It takes approximately 30 seconds to compile.

quill_v10_0_compiler_bench.speedscope.png

Verdict

Quill excels in hot path latency benchmarks and supports high throughput, offering a rich set of features that outshines other logging libraries.

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 amount 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 Quill were not available, MS BinLog would be a strong alternative. It delivers great latency on the hot path and generates smaller binary log files. However, the binary logs necessitate offline processing with additional tools, which can be less convenient.


🧩 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.png

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’s a sample CMakeLists.txt to get you started:

# 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

For a more integrated approach, embed Quill directly into your project:

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.1.0)
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

When building Quill for Android, you might need to add this flag during configuration, but in most cases, it works without it:

-DQUILL_NO_THREAD_NAME_SUPPORT:BOOL=ON

For timestamps, use quill::ClockSourceType::System. Quill also includes an AndroidSink, which integrates with 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

Easily integrate Quill with Meson’s wrapdb:

meson wrap install quill

Manual Integration

Copy the repository contents to your subprojects directory and add the following to your 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 Blzmod

Quill is available on BLZMOD for easy integration.

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

Frontend (caller-thread)

When invoking a LOG_ macro:

  1. Creates a static constexpr metadata object to store Metadata such as the format string and source location.

  2. Pushes the data SPSC lock-free queue. For each log message, the following variables are pushed

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

Backend

Consumes each message from the SPSC queue, retrieves all the necessary information and then formats the message. Subsequently, forwards the log message to all Sinks associated with the Logger.

design.jpg


🚨 Caveats

Quill may not work well with fork() since it spawns a background thread and fork() doesn't work well with multithreading.

If your application uses fork() and you want to log in the child processes as well, you should call quill::start() after the fork() call. Additionally, you should ensure that you write to different files in the parent and child processes to avoid conflicts.

For 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();
        
    // Get or create a handler to the file - Write to a different 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));

    QUILL_LOG_INFO(logger, "Hello from Child {}", 123);
  }
  else
  {
    quill::Backend::start();
          
    // Get or create a handler to the file - Write to a different 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));
    
    QUILL_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.