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);
}
}
Performance
Latency
The results presented in the tables below are measured in nanoseconds (ns)
.
Logging Numbers
LOG_INFO(logger, "Logging int: {}, int: {}, double: {}", i, j, d)
.
1 Thread Logging
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v4.4 Bounded Dropping Queue | 8 | 9 | 9 | 9 | 10 | 12 |
fmtlog | 8 | 9 | 10 | 10 | 11 | 13 |
Quill v4.4 Unbounded Queue | 11 | 11 | 11 | 12 | 13 | 17 |
PlatformLab NanoLog | 12 | 13 | 16 | 17 | 20 | 25 |
MS BinLog | 19 | 19 | 19 | 20 | 56 | 83 |
Reckless | 25 | 27 | 29 | 31 | 33 | 39 |
XTR | 6 | 6 | 39 | 42 | 47 | 59 |
Iyengar NanoLog | 89 | 102 | 124 | 132 | 231 | 380 |
spdlog | 147 | 151 | 155 | 158 | 166 | 174 |
g3log | 1167 | 1240 | 1311 | 1369 | 1593 | 1769 |
4 Threads Logging Simultaneously
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
XTR | 6 | 6 | 8 | 9 | 40 | 48 |
Quill v4.4 Bounded Dropping Queue | 8 | 9 | 9 | 10 | 11 | 13 |
fmtlog | 8 | 9 | 9 | 10 | 12 | 14 |
Quill v4.4 Unbounded Queue | 12 | 12 | 13 | 13 | 14 | 18 |
PlatformLab NanoLog | 13 | 15 | 18 | 21 | 25 | 28 |
Reckless | 17 | 21 | 24 | 25 | 28 | 47 |
MS BinLog | 19 | 19 | 20 | 21 | 58 | 88 |
Iyengar NanoLog | 94 | 105 | 135 | 144 | 228 | 314 |
spdlog | 209 | 248 | 297 | 330 | 423 | 738 |
g3log | 1253 | 1332 | 1393 | 1437 | 1623 | 2063 |
Logging Large Strings
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
.
1 Thread Logging
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v4.4 Bounded Dropping Queue | 10 | 11 | 12 | 13 | 13 | 16 |
fmtlog | 10 | 12 | 13 | 14 | 16 | 17 |
Quill v4.4 Unbounded Queue | 13 | 13 | 14 | 15 | 16 | 19 |
PlatformLab NanoLog | 15 | 18 | 22 | 25 | 29 | 34 |
MS BinLog | 20 | 21 | 22 | 23 | 58 | 86 |
XTR | 8 | 8 | 29 | 30 | 33 | 49 |
Reckless | 89 | 108 | 115 | 117 | 123 | 141 |
Iyengar NanoLog | 94 | 106 | 125 | 133 | 240 | 388 |
spdlog | 123 | 126 | 130 | 133 | 140 | 148 |
g3log | 890 | 966 | 1028 | 1119 | 1260 | 1463 |
4 Threads Logging Simultaneously
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v4.4 Bounded Dropping Queue | 11 | 11 | 13 | 13 | 14 | 17 |
XTR | 9 | 11 | 13 | 14 | 31 | 39 |
Quill v4.4 Unbounded Queue | 13 | 14 | 15 | 16 | 17 | 20 |
fmtlog | 12 | 13 | 16 | 16 | 19 | 21 |
MS BinLog | 21 | 22 | 23 | 25 | 60 | 90 |
PlatformLab NanoLog | 19 | 24 | 33 | 36 | 42 | 49 |
Reckless | 82 | 96 | 104 | 108 | 118 | 145 |
Iyengar NanoLog | 57 | 96 | 123 | 137 | 172 | 302 |
spdlog | 185 | 207 | 237 | 257 | 362 | 669 |
g3log | 983 | 1046 | 1112 | 1171 | 1376 | 1774 |
Logging Complex Types
LOG_INFO(logger, "Logging int: {}, int: {}, vector: {}", i, j, v)
.
Logging std::vector<std::string> v
containing 16 large strings, each ranging from 50 to 60 characters.
The strings used in the log message are over 35 characters to prevent the short string optimization of std::string
.
1 Thread Logging
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v4.4 Bounded Dropping Queue | 50 | 52 | 54 | 56 | 59 | 74 |
Quill v4.4 Unbounded Queue | 53 | 55 | 56 | 58 | 61 | 67 |
MS BinLog | 64 | 66 | 70 | 80 | 89 | 271 |
XTR | 282 | 290 | 338 | 343 | 350 | 575 |
fmtlog | 721 | 750 | 779 | 793 | 821 | 847 |
spdlog | 5881 | 5952 | 6026 | 6082 | 6342 | 6900 |
4 Threads Logging Simultaneously
Library | 50th | 75th | 90th | 95th | 99th | 99.9th |
---|---|---|---|---|---|---|
Quill v4.4 Bounded Dropping Queue | 53 | 55 | 57 | 59 | 62 | 80 |
MS BinLog | 66 | 68 | 71 | 74 | 87 | 295 |
Quill v4.4 Unbounded Queue | 88 | 95 | 103 | 108 | 119 | 135 |
XTR | 535 | 730 | 786 | 819 | 885 | 971 |
fmtlog | 788 | 811 | 831 | 844 | 872 | 906 |
spdlog | 6090 | 6165 | 6246 | 6337 | 7351 | 9322 |
The benchmark was conducted on Linux RHEL 9
with an Intel Core i5-12600
at 4.8 GHz.
The cpus are isolated on this system and each thread was pinned to a different CPU. GCC 13.1
was used as the compiler.
The benchmark methodology involved 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.
You can find the benchmark code on the logger_benchmarks repository.
Throughput
The maximum throughput is measured by determining the maximum number of log messages the backend logging thread can write to the log file per second.
When measured on the same system as the latency benchmarks mentioned above the average throughput of the backend
logging thread when formatting a log message consisting of an int and a double is ~4.40 million msgs/sec
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. The backend thread internally buffers the log messages
and then writes them later when the caller thread queues are empty or when a predefined limit,
backend_thread_transit_events_soft_limit
, is reached. This approach prevents the need for allocating new queues
or dropping messages on the hot path.
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 log guarantee, ensuring that every log message from the hot threads up to that point is flushed to the file.
For benchmarking purposes, you can find the code here.
Quick Start
#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
### Output
[![Screenshot-2020-08-14-at-01-09-43.png](http://i.postimg.cc/02Vbt8LH/Screenshot-2020-08-14-at-01-09-43.png)](http://postimg.cc/LnZ95M4z)
#### External CMake
##### Building and Installing Quill
git clone http://github.com/odygrd/quill.git mkdir cmake_build cd cmake_build cmake .. make install
Note: To install in custom directory invoke cmake with `-DCMAKE_INSTALL_PREFIX=/quill/install-dir/`
Then use the library from a CMake project, you can locate it directly with `find_package()`
##### Directory Structure
my_project/ ├── CMakeLists.txt ├── main.cpp
##### CMakeLists.txt
```cmake
# 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 PUBLIC quill::quill)
Embedded CMake
To embed the library directly, copy the source folder to your
project and call add_subdirectory()
in your CMakeLists.txt
file
Directory Structure
my_project/
├── quill/ (source folder)
├── CMakeLists.txt
├── main.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.1.0)
project(my_project)
set(CMAKE_CXX_STANDARD 17)
add_subdirectory(quill)
add_executable(my_project main.cpp)
target_link_libraries(my_project PUBLIC quill::quill)
Meson
Using WrapDB
Meson’s wrapdb
includes a quill package, which repackages quill to be built by meson as a subproject.
- Install quill subproject from the wrapdb by running from the root of your project
meson wrap install quill
Manual Integration
If you prefer not to use WrapDB, you can manually integrate Quill into your project by following these steps:
- Copy the contents of this repository under the subprojects directory in your project.
Bazel
Using Blzmod
The library is available on BLZMOD, allowing for easy integration into your project.
Manual Integration
If you prefer manual integration, you can add the library as a dependency in your BUILD.bazel
file. Below is a sample
cc_binary
rule demonstrating how to include the library. Ensure to replace //quill_path
with the actual path to the
directory containing the BUILD.bazel
file for the quill library within your project structure.
cc_binary(name = "app", srcs = ["main.cpp"], deps = ["//quill_path:quill"])
Design
Frontend (caller-thread)
When invoking a LOG_
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 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.