- Notifications
You must be signed in to change notification settings - Fork196
Asynchronous Low Latency C++ Logging Library
License
odygrd/quill
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Quill is ahigh-performance asynchronous logging library written inC++. 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.
- Extensive Documentation: Comprehensive guides and examples available.
- Community-Driven: Open to contributions, feedback, and feature requests.
Try it onCompiler Explorer
Getting started is easy and straightforward. Follow these steps to integrate the library into your project:
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 |
Once installed, you can start using Quill with the following code:
#include"quill/Backend.h"#include"quill/Frontend.h"#include"quill/LogMacros.h"#include"quill/Logger.h"#include"quill/sinks/ConsoleSink.h"#include<string_view>intmain(){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"});}
- High-Performance: Ultra-low latency performance. ViewBenchmarks
- Asynchronous Processing: Background thread handles formatting and I/O, keeping your main thread responsive.
- Minimal Header Includes:
- Frontend: Only
Logger.h
andLogMacros.h
needed for logging. Lightweight with minimal dependencies. - Backend: Single
.cpp
file 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.SeeFormatters.
- Timestamp-Ordered Logs: Simplify debugging of multithreaded applications with chronologically ordered logs.
- 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.SeeBacktrace Logging
- Multiple Output Sinks: Console (with color), files (with rotation), JSON, ability to create custom sinks and more.
- Log Filtering: Process only relevant messages.SeeFilters.
- JSON Logging: Structured log output.SeeJSON Logging
- Configurable Queue Modes:
bounded/unbounded
andblocking/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 widestrings.
- 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.
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/cmdlineBOOT_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 thelogger_benchmarks repository.
The results presented in the tables below are measured innanoseconds (ns)
.
The tables are sorted by the 95th percentile
LOG_INFO(logger, "Logging int: {}, int: {}, double: {}", i, j, d)
.
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill Bounded Dropping Queue | 7 | 8 | 8 | 9 | 9 | 11 |
fmtlog | 9 | 9 | 10 | 10 | 12 | 13 |
Quill Unbounded Queue | 10 | 10 | 10 | 10 | 12 | 14 |
PlatformLab NanoLog | 13 | 14 | 16 | 17 | 19 | 25 |
MS BinLog | 21 | 21 | 22 | 22 | 56 | 93 |
XTR | 7 | 7 | 29 | 30 | 33 | 53 |
Reckless | 26 | 28 | 31 | 32 | 35 | 49 |
BqLog | 29 | 29 | 30 | 49 | 56 | 71 |
Iyengar NanoLog | 83 | 96 | 117 | 125 | 152 | 197 |
spdlog | 143 | 147 | 152 | 158 | 165 | 177 |
g3log | 1161 | 1259 | 1329 | 1419 | 1602 | 1827 |
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
fmtlog | 8 | 9 | 9 | 10 | 11 | 13 |
Quill Bounded Dropping Queue | 8 | 9 | 10 | 10 | 12 | 14 |
XTR | 7 | 8 | 9 | 11 | 31 | 38 |
Quill Unbounded Queue | 10 | 11 | 11 | 12 | 13 | 15 |
PlatformLab NanoLog | 15 | 17 | 20 | 23 | 27 | 32 |
MS BinLog | 21 | 22 | 22 | 23 | 62 | 100 |
Reckless | 19 | 23 | 26 | 28 | 34 | 55 |
BqLog | 31 | 33 | 34 | 55 | 61 | 73 |
Iyengar NanoLog | 58 | 90 | 123 | 131 | 168 | 242 |
spdlog | 210 | 243 | 288 | 313 | 382 | 694 |
g3log | 1271 | 1337 | 1396 | 1437 | 1614 | 1899 |
Loggingstd::string
over 35 characters to prevent the short string optimization.
LOG_INFO(logger, "Logging int: {}, int: {}, string: {}", i, j, large_string)
.
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill Bounded Dropping Queue | 11 | 13 | 13 | 14 | 15 | 16 |
fmtlog | 11 | 12 | 13 | 14 | 15 | 17 |
Quill Unbounded Queue | 14 | 15 | 16 | 17 | 18 | 19 |
MS BinLog | 22 | 23 | 24 | 25 | 61 | 100 |
PlatformLab NanoLog | 15 | 17 | 21 | 27 | 33 | 39 |
XTR | 8 | 9 | 29 | 31 | 35 | 54 |
BqLog | 29 | 30 | 31 | 51 | 60 | 71 |
Reckless | 91 | 107 | 115 | 118 | 124 | 135 |
Iyengar NanoLog | 86 | 97 | 119 | 128 | 159 | 268 |
spdlog | 120 | 124 | 128 | 132 | 141 | 151 |
g3log | 881 | 956 | 1018 | 1089 | 1264 | 1494 |
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
XTR | 9 | 11 | 13 | 14 | 32 | 40 |
fmtlog | 11 | 12 | 13 | 14 | 16 | 19 |
Quill Bounded Dropping Queue | 13 | 14 | 15 | 16 | 17 | 19 |
Quill Unbounded Queue | 15 | 16 | 17 | 18 | 19 | 21 |
MS BinLog | 23 | 25 | 27 | 28 | 65 | 105 |
PlatformLab NanoLog | 16 | 20 | 32 | 38 | 44 | 51 |
BqLog | 32 | 33 | 35 | 56 | 64 | 76 |
Reckless | 79 | 94 | 104 | 107 | 114 | 132 |
Iyengar NanoLog | 85 | 93 | 125 | 133 | 168 | 237 |
spdlog | 178 | 218 | 261 | 281 | 381 | 651 |
g3log | 992 | 1055 | 1121 | 1178 | 1360 | 1600 |
Loggingstd::vector<std::string>
containing 16 large strings, each ranging from 50 to 60 characters.
Note: some of the previous loggers do not support passing astd::vector
as an argument.
LOG_INFO(logger, "Logging int: {}, int: {}, vector: {}", i, j, v)
.
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill Bounded Dropping Queue | 48 | 50 | 53 | 55 | 58 | 62 |
Quill Unbounded Queue | 54 | 56 | 57 | 58 | 61 | 66 |
MS BinLog | 68 | 69 | 72 | 74 | 79 | 281 |
XTR | 284 | 294 | 340 | 346 | 356 | 575 |
fmtlog | 711 | 730 | 754 | 770 | 804 | 834 |
spdlog | 6191 | 6261 | 6330 | 6386 | 6633 | 7320 |
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill Bounded Dropping Queue | 50 | 52 | 54 | 56 | 60 | 82 |
MS BinLog | 70 | 72 | 75 | 79 | 88 | 286 |
Quill Unbounded Queue | 97 | 107 | 116 | 122 | 135 | 148 |
XTR | 512 | 711 | 761 | 791 | 865 | 945 |
fmtlog | 780 | 804 | 823 | 835 | 860 | 896 |
spdlog | 6469 | 6549 | 6641 | 6735 | 7631 | 9430 |
The benchmark methodology involves logging 20 messages in a loop, calculating and storing the average latency for those20 messages, then waiting around ~2 milliseconds, and repeating this process for a specified number of iterations.
In theQuill Bounded Dropping
benchmarks, the dropping queue size is set to262,144
bytes, which is double thedefault size of131,072
bytes.
Throughput is measured by calculating the maximum number of log messages the backend logging thread can write to a logfile per second.
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 multiplethreads. Benchmarking throughput of asynchronous logging libraries presents certain challenges. Some libraries may droplog messages, leading to smaller-than-expected log files, while others only provide asynchronous flushing, making itdifficult 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 aflush-and-wait mechanism.
Note thatMS BinLog
writes log data to a binary file, which requires offline formatting with an additionalprogram—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 unlessprocessed offline. However, it is included for reference. The other version ofBqLog
is using a text appender andproduces human-readable log files.
In the same way,Platformlab Nanolog
also outputs binary logs and is expected to deliver high throughput. However, forreasons unexplained, the benchmark runs significantly slower (10x longer) than the other libraries, so it is excludedfrom the table.
Logging 4 million times the message"Iteration: {} int: {} double: {}"
Library | million msg/second | elapsed time |
---|---|---|
MS BinLog (binary log) | 63.80 | 62 ms |
BqLog (binary log) | 15.92 | 251 ms |
Quill | 5.70 | 701 ms |
BqLog | 4.93 | 811 ms |
spdlog | 3.54 | 1128 ms |
fmtlog | 2.90 | 1378 ms |
Reckless | 2.72 | 1471 ms |
XTR | 2.61 | 1534 ms |
Compile times are measured usingclang 15
and forRelease
build.
Below, you can find the additional headers that the library will include when you need to log, followingtherecommended_usageexample
There is also a compile-time benchmark measuring the compilation time of 2000 auto-generated log statements withvarious arguments. You can findithere. It takesapproximately 30 seconds to compile.
Quill excels in hot path latency benchmarks and supports high throughput, offering a rich set of features that outshinesother logging libraries.
The human-readable log files facilitate easier debugging and analysis. While initially larger, they compressefficiently, 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 MBms_binlog_backend_total_time.zip (zipped binary log): 35 MB
quill_backend_total_time.log (human-readable log): 448 MBquill_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 andgenerates smaller binary log files. However, the binary logs necessitate offline processing with additional tools, whichcan be less convenient.
Also, see theQuick 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>intmain(){// Backend quill::BackendOptions backend_options;quill::Backend::start(backend_options);// Frontendauto 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 123int a =123; std::string l ="log";LOG_INFO(logger,"A {} message with number {}", l, a);// libfmt formatting language is supported 3.14e+00doublepi =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 secondLOG_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.");}
To get started with Quill, clone the repository and install it using CMake:
git clone http://github.com/odygrd/quill.gitmkdir cmake_buildcd cmake_buildcmake ..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 usingfind_package()
:
find_package(quill REQUIRED)target_link_libraries(your_targetPUBLIC quill::quill)
Organize your project directory like this:
my_project/├── CMakeLists.txt├── main.cpp
Here’s a sampleCMakeLists.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(examplePUBLIC quill::quill)
For a more integrated approach, embed Quill directly into your project:
my_project/├── quill/ # Quill repo folder├── CMakeLists.txt├── main.cpp
Use thisCMakeLists.txt
to include Quill directly:
cmake_minimum_required(VERSION 3.1.0)project(my_project)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIREDON)add_subdirectory(quill)add_executable(my_project main.cpp)target_link_libraries(my_projectPUBLIC quill::quill)
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, usequill::ClockSourceType::System
. Quill also includes anAndroidSink
, which integrates with Android's logging system.
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);
Easily integrate Quill with Meson’swrapdb
:
meson wrap install quill
Copy the repository contents to yoursubprojects
directory and add the following to yourmeson.build
:
quill=subproject('quill')quill_dep= quill.get_variable('quill_dep')my_build_target=executable('name','main.cpp',dependencies: [quill_dep],install:true)
Quill is available onBLZMOD
for easy integration.
For manual setup, add Quill to yourBUILD.bazel
file like this:
cc_binary(name="app",srcs= ["main.cpp"],deps= ["//quill_path:quill"])
When invoking aLOG_
macro:
Creates a static constexpr metadata object to store
Metadata
such as the format string and source location.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 theLOG_ macro |
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.
Quill may not work well withfork()
since it spawns a background thread andfork()
doesn't work well withmultithreading.
If your application usesfork()
and you want to log in the child processes as well, you should callquill::start()
after thefork()
call. Additionally, you should ensure that you write to different files in the parent and childprocesses 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"intmain(){// 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 fileauto 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 fileauto 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); }}
Quill is licensed under theMIT 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.
About
Asynchronous Low Latency C++ Logging Library