Skip to content

odygrd/quill

Repository files navigation

Quill logo

Quill

Asynchronous Low Latency C++ Logging Library



Package Manager Installation Command
vcpkg vcpkg install quill
conan quill/[>=1.2.3]
homebrew brew install quill
meson wrapdb meson wrap install quill

Introduction

Quill is a high-performance, cross-platform logging library designed for C++17 and onwards. 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.

Documentation

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.

Features

  • Low Latency Logging: Achieve fast logging performance with low latency. Refer to the Benchmarks for more details.
  • Asynchronous logging: Log arguments and messages are formatted in a backend logging thread, effectively offloading the formatting overhead from the critical path.
  • 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 log message 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 Sinks: Utilize various logging targets, including:
    • Console logging with color support.
    • File logging.
    • Rotating log files based on time or size.
    • JSON logging.
    • Custom sinks.
  • 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 or dropping message modes in the library. In blocking mode, the hot threads pause and wait when the queue is full until space becomes available, ensuring no message loss but introducing potential latency. In dropping 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 different 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: Wide strings compatible with ASCII encoding are supported, applicable to Windows only. Additionally, there is support for logging STL containers consisting of wide strings. Note that chaining STL types, such as std::vector<std::vector<std::wstring>> is not supported for wide strings.
  • 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 API: Type safe api 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.

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

The following message is logged 100'000 times for each thread:

LOG_INFO(logger, "Logging int: {}, int: {}, double: {}", i, j, d).

1 Thread Logging
Library 50th 75th 90th 95th 99th 99.9th
Quill v4.2 Bounded Dropping Queue 6 7 8 8 9 10
Quill v4.2 Unbounded Queue 8 8 9 9 10 13
Quill v3.8 Unbounded Queue 8 8 9 9 10 13
fmtlog 8 8 9 9 10 13
PlatformLab NanoLog 11 12 13 14 15 20
MS BinLog 21 21 22 24 28 57
Reckless 41 45 47 48 49 69
Iyengar NanoLog 51 54 63 81 113 160
spdlog 148 153 159 163 169 176
g3log 1192 1282 1363 1440 1624 1802
4 Threads Logging Simultaneously
Library 50th 75th 90th 95th 99th 99.9th
Quill v4.2 Bounded Dropping Queue 7 8 9 9 10 12
fmtlog 8 8 9 9 11 13
Quill v3.8 Unbounded Queue 8 9 10 10 11 13
Quill v4.2 Unbounded Queue 9 9 10 11 12 15
PlatformLab NanoLog 12 13 13 14 15 19
MS BinLog 21 21 22 22 29 62
Reckless 42 46 47 48 54 78
Iyengar NanoLog 53 62 93 122 150 216
spdlog 209 236 276 304 409 700
g3log 1344 1415 1489 1557 1815 5855

Logging Large Strings

The following message is logged 100'000 times for each 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.

1 Thread Logging
Library 50th 75th 90th 95th 99th 99.9th
Quill v3.8 Unbounded Queue 10 12 13 13 14 16
Quill v4.2 Bounded Dropping Queue 11 12 13 14 15 17
fmtlog 11 12 13 14 15 17
PlatformLab NanoLog 13 14 15 15 17 19
Quill v4.2 Unbounded Queue 13 14 16 16 17 21
MS BinLog 22 23 23 25 30 59
Iyengar NanoLog 52 55 64 83 114 160
Reckless 102 122 134 137 143 153
spdlog 120 123 127 130 138 145
g3log 955 1049 1129 1190 1351 1539
4 Threads Logging Simultaneously
Library 50th 75th 90th 95th 99th 99.9th
Quill v4.2 Bounded Dropping Queue 11 12 13 15 16 18
fmtlog 11 12 13 15 16 18
Quill v3.8 Unbounded Queue 12 13 14 15 16 19
PlatformLab NanoLog 13 15 15 16 17 20
Quill v4.2 Unbounded Queue 14 16 17 18 19 22
MS BinLog 23 24 24 25 31 62
Iyengar NanoLog 53 60 92 121 149 212
Reckless 101 121 133 136 143 160
spdlog 186 215 266 297 381 641
g3log 1089 1164 1252 1328 1578 5563

Logging Complex Types

The following message is logged 100'000 times for each thread:

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.2 Bounded Dropping Queue 52 54 56 58 60 71
Quill v4.2 Unbounded Queue 53 56 57 59 61 64
MS BinLog 66 70 79 81 84 91
Quill v3.8 Unbounded Queue 632 651 676 698 737 1049
fmtlog 724 752 776 789 814 857
spdlog 6242 6331 6438 6523 6782 7341
4 Threads Logging Simultaneously
Library 50th 75th 90th 95th 99th 99.9th
Quill v4.2 Bounded Dropping Queue 52 55 57 59 62 77
MS BinLog 70 74 83 85 88 102
Quill v4.2 Unbounded Queue 86 94 103 110 122 138
Quill v3.8 Unbounded Queue 674 694 736 762 805 884
fmtlog 789 813 833 845 872 908
spdlog 6500 6596 6724 6848 7560 9036

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 is 4.56 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.

Compilation Time

Compile times are measured using clang 15 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_v4_1_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_v4_compiler_bench.speedscope.png

Quick Start

#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
#include "quill/sinks/FileSink.h"

int main()
{
  // Start the backend thread
  quill::Backend::start();

  // Log to file
  auto file_sink = quill::Frontend::create_or_get_sink<quill::FileSink>(
    "example_file_logging.log");

  quill::Logger* logger =
    quill::Frontend::create_or_get_logger("root", std::move(file_sink));

  // set the log level of the logger to trace_l3 (default is info)
  logger->set_log_level(quill::LogLevel::TraceL3);
  
  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/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
#include "quill/sinks/ConsoleSink.h"

int main()
{
  // Start the backend thread
  quill::Backend::start();

  // 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);

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

Output

Screenshot-2020-08-14-at-01-09-43.png

Usage

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
# 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

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
  • In your project’s meson.build file, add an entry for the new subproject
quill = subproject('quill')
quill_dep = quill.get_variable('quill_dep')
  • Include the new dependency object to link with quill
my_build_target = executable('name', 'main.cpp', dependencies : [quill_dep], install : true)

Building Quill for Android NDK

To build quill for Android NDK add the following flags when configuring the build:

-DQUILL_NO_THREAD_NAME_SUPPORT:BOOL=ON

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

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.