homebrew | vcpkg | conan |
---|---|---|
brew install quill |
vcpkg install quill |
quill/[>=1.2.3] |
Quill is a cross-platform low latency logging library based on C++14/C++17.
There are two versions on the library:
v1.7
: C++14 - Bug fix only
v2
: C++17 - New features and actively maintained
The examples folder is also a good source of documentation.
- Low latency logging See Benchmarks.
- Format outside the hot-path in a backend logging thread. For
non-built-in
typesostream::operator<<()
is called on a copy of the object by the backend logging thread. Unsafe to copynon-trivial user defined
are detected in compile time. Those types can be tagged assafe-to-copy
to avoid formatting them on the hot path. See User Defined Types. - Custom formatters. Logs can be formatted based on a user specified pattern. See Formatters.
- Support for rdtsc, chrono or custom clock (usefull for simulations) for timestamp generation.
- Support for log stack traces. Store log messages in a ring buffer and display later on a higher severity log statement or on demand. See Backtrace Logging.
- Various logging targets. See Handlers.
- Console logging with colours support.
- File Logging
- Rotating log files
- Time rotating log files
- JSON logging
- Custom Handlers
- Filters for filtering log messages. See Filters.
- Ability to produce JSON structured log. See Structured-Log
guaranteed non-blocking
ornon-guaranteed
logging. Innon-guaranteed
mode there is no heap allocation of a new queue but log messages can be dropped. See FAQ.- Support for wide character logging and wide character filenames (Windows and v1.7.x only).
- Log statements in timestamp order even when produced by different threads. This makes debugging multithreading applications easier.
- Log levels can be completely stripped out at compile time reducing
if
branches. - Clean warning-free codebase even on high warning levels.
- Crash safe behaviour with a built-in signal handler.
- Type safe python style API with compile type checks and built-in support for logging STL types/containers by using the excellent {fmt} library.
The following message is logged 100'000 times per
thread LOG_INFO(logger, "Logging int: {}, int: {}, double: {}", i, j, d)
.
The results in the tables below are in nanoseconds (ns).
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v2.8.0 Unbounded Queue | 20 | 21 | 24 | 25 | 27 | 34 |
Quill v2.8.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 v2.8.0 Unbounded Queue | 20 | 22 | 24 | 26 | 28 | 35 |
Quill v2.8.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 is over 35 characters to avoid short string optimisation of std::string
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v2.8.0 Unbounded Queue | 31 | 33 | 35 | 36 | 39 | 48 |
Quill v2.8.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 v2.8.0 Unbounded Queue | 31 | 33 | 35 | 37 | 40 | 48 |
Quill v2.8.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 benchmarks are done on Ubuntu - Intel(R) Xeon(R) Gold 6254 CPU @ 3.10GHz
with GCC 12.2
Each thread is pinned on a different cpu. Unfortunately the cores are not isolated on this system. If the backend logging thread is run in the same CPU as the caller hot-path threads, that slows down the log message processing on the backend logging thread and will cause the SPSC queue to fill faster and re-allocate.
Continuously logging messages in a loop makes the consumer (backend logging thread) unable to follow up and the queue will have to re-allocate or block for most logging libraries expect very high throughput binary loggers like PlatformLab Nanolog.
Therefore, a different approach was followed that suits more to a real time application:
- 20 messages are logged in a loop.
- calculate/store the average latency for those messages.
- wait between 1-2 ms.
- repeat for n iterations.
I run each logger benchmark 4 times and the above latencies are the second-best result.
The benchmark code and results can be found here.
The main focus of the library is not throughput. The backend logging thread is a single thread responsible for
formatting, ordering the log messages from multiple hot threads and finally outputting everything as human-readable
text.
The logging thread always empties all the queues of the hot threads on the highest priority (to avoid allocating a new
queue or dropping messages on the hot path). To achieve that, it internally buffers the log messages and then
writes them later when the hot thread queues are empty or when a limit is
reached backend_thread_transit_events_soft_limit
.
I haven't found an easy way to compare the throughput against other logging libraries while doing asynchronous logging. For example some libraries will drop the log messages ending in producing much smaller log files than the expected, other libraries only offer an async flush meaning that you never really know when the logging thread has finished processing everything.
Quill has a blocking flush()
guaranteeing every single log message from the hot threads up to that point is flushed to
the file.
The maximum throughput is measured as the max log messages number the backend logging thread can write to the file per
second.
Benchmark code can be found here
Measured on the same system as the latency benchmarks above for 4 million messages produces a log file of 476 mb
1.76 million msgs/sec average, total time elapsed 2266 ms, total log messages 4 million
#include "quill/Quill.h"
int main()
{
quill::Config cfg;
cfg.enable_console_colours = true;
quill::configure(cfg);
quill::start();
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(2, 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
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.