- Introduction
- Documentation
- Features
- Caveats
- Performance
- Basic Usage
- CMake Integration
- Design
- License
homebrew | vcpkg | conan |
---|---|---|
brew install quill |
vcpkg install quill |
quill/[>=1.2.3] |
Quill is a high-performance, cross-platform logging library designed for C++14 and onwards. It provides two versions of the library:
v1.7
: This version is based on C++14 and focuses on bug fixes and stability improvements.v2
and onwards: Starting from version 2, Quill is based on C++17 and includes new features and ongoing maintenance.
Quill is a production-ready logging library that has undergone extensive unit testing. It has been successfully utilized in production environments, including financial trading applications, providing high-performance and reliable logging capabilities.
For detailed documentation and usage instructions, please visit the Quill Documentation on Read the Docs. It provides comprehensive information on how to integrate and utilize Quill in your C++ applications.
Additionally, you can explore the examples folder in the Quill repository on GitHub. These examples serve as valuable resources to understand different usage scenarios and demonstrate the capabilities of the library.
- Low Latency Logging: Achieve fast logging performance with low latency. Refer to the Benchmarks for more details.
- Backend Logging Thread: Format logs outside the critical path in a backend logging thread. For
non-built-in
types, the backend logging thread invokesostream::operator<<()
on a copy of the object. Compile-time detection of unsafe copying fornon-trivial user defined
types is supported. To avoid formatting them on the critical path, such types can be tagged assafe-to-copy
. See User Defined Types for more information. - Custom Formatters: Customize log formatting based on user-defined patterns. Explore Formatters for further details.
- Flexible Timestamp Generation: Choose between rdtsc, chrono, or custom clocks (useful for simulations) for timestamp generation.
- Log Stack Traces: Store log messages in a ring buffer and display them later in response to a higher severity log statement or on demand. Refer to Backtrace Logging for more information.
- Multiple Logging Targets: Utilize various logging targets, including:
- Console logging with color support.
- File logging.
- Rotating log files.
- Time rotating log files.
- JSON logging.
- Custom handlers.
- Log Message Filtering: Apply filters to selectively process log messages. Learn more about Filters.
- Structured Logging: Generate JSON structured logs. See Structured-Log for details.
- Blocking or Dropping Message Modes: Choose between
blocking
ordropping
message modes in the library. Inblocking
mode, the hot threads pause and wait when the queue is full until space becomes available, ensuring no message loss but introducing potential latency. Indropping
mode, log messages beyond the queue's capacity may be dropped to prioritize low latency. The library provides reports on dropped messages, queue reallocations, and blocked hot threads for monitoring purposes. - Queue Types: The library supports two types of queues for transferring logs from the hot path to the backend thread: bounded queues with a fixed capacity and unbounded queues that start small and can dynamically grow.
- Wide Character Support: Log messages and filenames with wide characters are supported (Windows and v1.7.x only).
- Ordered Log Statements: Log statements are ordered by timestamp even when produced by different threads, facilitating easier debugging of multithreaded applications.
- Compile-Time Log Level Stripping: Completely strip out log levels at compile time, reducing
if
branches. - Clean and Warning-Free Codebase: Ensure a clean and warning-free codebase, even with high compiler warning levels.
- Crash-Safe Behavior: Benefit from crash-safe behavior with a built-in signal handler.
- Type-Safe Python-Style API: Utilize a type-safe API inspired by Python, with compile-time checks and built-in support for logging STL types/containers using the excellent {fmt} library.
- Huge Pages: Benefit from support for huge pages on the hot path. This feature allows for improved performance and efficiency.
- Printf-style format: The library offers support for the
printf
style format in addition to the existinglibfmt
style format. This feature is particularly useful when migrating from legacy codebases that rely on theprintf
style format.
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/Quill.h"
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// DO NOT CALL THIS BEFORE FORK
// quill::start();
if (fork() == 0)
{
// Get or create a handler to the file - Write to a different file
std::shared_ptr<quill::Handler> file_handler =
quill::file_handler("child_log.log", "w", quill::FilenameAppend::DateTime);
quill::Config cfg;
cfg.default_handlers.push_back(std::move(file_handler));
quill::configure(cfg);
quill::start();
QUILL_LOG_INFO(quill::get_logger(), "Hello from Child {}", 123);
}
else
{
// Get or create a handler to the file - Write to a different file
std::shared_ptr<quill::Handler> file_handler =
quill::file_handler("parent_log.log", "w", quill::FilenameAppend::DateTime);
quill::Config cfg;
cfg.default_handlers.push_back(std::move(file_handler));
quill::configure(cfg);
quill::start();
QUILL_LOG_INFO(quill::get_logger(), "Hello from Parent {}", 123);
}
}
The following message is logged 100,000 times per
thread: LOG_INFO(logger, "Logging int: {}, int: {}, double: {}", i, j, d)
.
The results presented in the tables below are measured in nanoseconds (ns).
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v3.0.0 Unbounded Queue | 20 | 21 | 24 | 25 | 27 | 34 |
Quill v3.0.0 Bounded Queue | 17 | 19 | 21 | 22 | 26 | 36 |
fmtlog | 16 | 19 | 21 | 22 | 27 | 40 |
MS BinLog | 41 | 43 | 44 | 46 | 66 | 118 |
PlatformLab NanoLog | 53 | 66 | 75 | 80 | 92 | 106 |
Reckless | 62 | 75 | 79 | 84 | 94 | 103 |
Iyengar NanoLog | 164 | 186 | 213 | 232 | 305 | 389 |
spdlog | 694 | 761 | 838 | 887 | 996 | 1143 |
g3log | 5398 | 5639 | 5875 | 6025 | 6327 | 6691 |
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v3.0.0 Unbounded Queue | 20 | 22 | 24 | 26 | 28 | 35 |
Quill v3.0.0 Bounded Queue | 17 | 19 | 21 | 22 | 26 | 36 |
fmtlog | 16 | 19 | 21 | 23 | 26 | 35 |
MS BinLog | 42 | 44 | 46 | 48 | 76 | 118 |
PlatformLab NanoLog | 56 | 67 | 77 | 82 | 95 | 159 |
Reckless | 46 | 62 | 78 | 92 | 113 | 155 |
Iyengar NanoLog | 150 | 168 | 247 | 289 | 355 | 456 |
spdlog | 728 | 828 | 907 | 959 | 1140 | 1424 |
g3log | 5103 | 5318 | 5525 | 5657 | 5927 | 6279 |
The following message is logged 100,000 times per
thread: LOG_INFO(logger, "Logging int: {}, int: {}, string: {}", i, j, large_string)
.
The large string used in the log message is over 35 characters to prevent the short string optimization
of std::string
.
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v3.0.0 Unbounded Queue | 31 | 33 | 35 | 36 | 39 | 48 |
Quill v3.0.0 Bounded Queue | 30 | 32 | 33 | 35 | 43 | 51 |
fmtlog | 29 | 31 | 34 | 37 | 44 | 53 |
MS BinLog | 50 | 51 | 53 | 56 | 77 | 127 |
PlatformLab NanoLog | 71 | 86 | 105 | 117 | 136 | 158 |
Reckless | 215 | 242 | 268 | 284 | 314 | 517 |
Iyengar NanoLog | 172 | 191 | 218 | 238 | 312 | 401 |
spdlog | 653 | 708 | 770 | 831 | 950 | 1083 |
g3log | 4802 | 4998 | 5182 | 5299 | 5535 | 5825 |
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v3.0.0 Unbounded Queue | 31 | 33 | 35 | 37 | 40 | 48 |
Quill v3.0.0 Bounded Queue | 29 | 31 | 33 | 35 | 41 | 49 |
fmtlog | 29 | 31 | 35 | 37 | 44 | 54 |
MS BinLog | 50 | 52 | 54 | 58 | 86 | 130 |
PlatformLab NanoLog | 69 | 82 | 99 | 111 | 134 | 194 |
Reckless | 187 | 209 | 232 | 247 | 291 | 562 |
Iyengar NanoLog | 159 | 173 | 242 | 282 | 351 | 472 |
spdlog | 679 | 751 | 839 | 906 | 1132 | 1478 |
g3log | 4739 | 4955 | 5157 | 5284 | 5545 | 5898 |
The benchmark was conducted on an Ubuntu system with an Intel Xeon Gold 6254 CPU running at 3.10GHz. The benchmark used GCC 12.2 as the compiler.
To ensure accurate performance measurement, each thread was pinned to a different CPU. The cores are also isolated on this particular system.
The benchmark methodology involved logging 20 messages in a loop, calculating and storing the average latency for those messages, waiting between 1-2 milliseconds, and repeating this process for a specified number of iterations.
The benchmark was executed four times for each logging library, and the reported latencies represent the second-best result obtained.
You can find the benchmark code on the logger_benchmarks repository.
While the primary focus of the library is not on throughput, it does provide efficient handling of log messages across
multiple threads. The backend logging thread, responsible for formatting and ordering log messages from hot threads,
ensures that all queues are emptied on a high priority basis. This approach prevents the need for allocating new queues
or dropping messages on the hot path. Instead, the backend thread internally buffers log messages and writes them when
the hot thread queues are empty or when a predefined limit, backend_thread_transit_events_soft_limit
, is reached.
Comparing throughput with other logging libraries in an asynchronous logging scenario has proven challenging. Some libraries may drop log messages, resulting in smaller log files than expected, while others only offer asynchronous flush, making it difficult to determine when the logging thread has finished processing all messages.
In contrast, Quill provides a blocking flush()
guarantee, ensuring that every log message from the hot threads up to
that point is flushed to the file. The maximum throughput is measured by determining the maximum number of log messages
the backend logging thread can write to the file per second.
For benchmarking purposes, you can find the code here. When measured on the same system as the latency benchmarks mentioned earlier, logging 4 million messages resulted in a log file size of 476 MB. The average throughput achieved was 1.76 million messages per second, with a total elapsed time of 2266 ms.
Please note that while Quill performs well in terms of throughput, its primary strength lies in its efficient handling of log messages across threads.
#include "quill/Quill.h"
int main()
{
quill::configure(
[]()
{
// See Config.h and Tweaks.h for run time and compile time configuration options
quill::Config cfg;
return cfg;
}());
// Starts the logging backend thread
quill::start();
// Create a file logger
quill::Logger* logger = quill::create_logger(
"file_logger",
quill::file_handler("example.log",
[]()
{
quill::FileHandlerConfig cfg;
cfg.set_open_mode('w');
cfg.set_pattern(
"[%(ascii_time)] [%(thread)] [%(filename):%(lineno)] [%(logger_name)] "
"[%(level_name)] - %(message)",
"%H:%M:%S.%Qms");
return cfg;
}()));
logger->set_log_level(quill::LogLevel::TraceL3);
// enable a backtrace that will get flushed when we log CRITICAL
logger->init_backtrace(2u, quill::LogLevel::Critical);
LOG_BACKTRACE(logger, "Backtrace log {}", 1);
LOG_BACKTRACE(logger, "Backtrace log {}", 2);
LOG_INFO(logger, "Welcome to Quill!");
LOG_ERROR(logger, "An error message. error code {}", 123);
LOG_WARNING(logger, "A warning message.");
LOG_CRITICAL(logger, "A critical error.");
LOG_DEBUG(logger, "Debugging foo {}", 1234);
LOG_TRACE_L1(logger, "{:>30}", "right aligned");
LOG_TRACE_L2(logger, "Positional arguments are {1} {0} ", "too", "supported");
LOG_TRACE_L3(logger, "Support for floats {:03.2f}", 1.23456);
}
#include "quill/Quill.h"
int main()
{
quill::Config cfg;
cfg.enable_console_colours = true;
quill::configure(cfg);
quill::start();
// Default root logger to stdout
quill::Logger* logger = quill::get_logger();
logger->set_log_level(quill::LogLevel::TraceL3);
// enable a backtrace that will get flushed when we log CRITICAL
logger->init_backtrace(2u, quill::LogLevel::Critical);
LOG_BACKTRACE(logger, "Backtrace log {}", 1);
LOG_BACKTRACE(logger, "Backtrace log {}", 2);
LOG_INFO(logger, "Welcome to Quill!");
LOG_ERROR(logger, "An error message. error code {}", 123);
LOG_WARNING(logger, "A warning message.");
LOG_CRITICAL(logger, "A critical error.");
LOG_DEBUG(logger, "Debugging foo {}", 1234);
LOG_TRACE_L1(logger, "{:>30}", "right aligned");
LOG_TRACE_L2(logger, "Positional arguments are {1} {0} ", "too", "supported");
LOG_TRACE_L3(logger, "Support for floats {:03.2f}", 1.23456);
}
git clone http://github.com/odygrd/quill.git
mkdir cmake_build
cd cmake_build
make install
Note: To install in custom directory invoke cmake with -DCMAKE_INSTALL_PREFIX=/quill/install-dir/
cmake -DCMAKE_PREFIX_PATH=/my/fmt/fmt-config.cmake-directory/ -DQUILL_FMT_EXTERNAL=ON -DCMAKE_INSTALL_PREFIX=/quill/install-dir/'
Then use the library from a CMake project, you can locate it directly with find_package()
my_project/
├── CMakeLists.txt
├── main.cpp
# Set only if needed - quill was installed under a custom non-standard directory
set(CMAKE_PREFIX_PATH /test_quill/usr/local/)
find_package(quill REQUIRED)
# Linking your project against quill
add_executable(example main.cpp)
target_link_libraries(example PRIVATE quill::quill)
See basic usage
To embed the library directly, copy the source folder to your
project and call add_subdirectory()
in your CMakeLists.txt
file
my_project/
├── quill/ (source folder)
├── CMakeLists.txt
├── main.cpp
cmake_minimum_required(VERSION 3.1.0)
project(my_project)
set(CMAKE_CXX_STANDARD 14)
add_subdirectory(quill)
add_executable(my_project main.cpp)
target_link_libraries(my_project PRIVATE quill::quill)
See basic usage
To build Quill as a shared library (DLL) on Windows, follow these steps:
-
Add the following CMake flags when configuring the build:
-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE -DBUILD_SHARED_LIBS=ON
-
Additionally, you need to define
QUILL_BUILD_SHARED
either in your code before includingQuill.h
or as a compiler flag when building outside of CMake.
To build Quill for Android NDK add the following CMake flags when configuring the build:
-DQUILL_NO_THREAD_NAME_SUPPORT:BOOL=ON
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.