odygrd / quill

Asynchronous Low Latency C++ Logging Library
MIT License
1.14k stars 130 forks source link
async asynchronous cpp cpp14 cpp17 cpp20 cross-platform fmtlib high-performance log-library logger logging logging-library low-latency
Quill logo

Quill

linux-ci macos-ci windows-ci
Codecov Codacy CodeFactor
license language

Asynchronous Low Latency C++ Logging Library



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

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

Quill Documentation

Additionally, you can explore the examples folder on GitHub. These examples serve as valuable resources to understand different usage scenarios and demonstrate the capabilities of the library.

Features

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 v5.1 Bounded Dropping Queue 6 7 8 8 9 10
Quill v5.1 Unbounded Queue 8 9 9 9 10 11
fmtlog 8 9 10 10 11 13
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
Quill v5.1 Bounded Dropping Queue 7 8 8 9 10 11
XTR 6 6 8 9 40 48
fmtlog 8 9 9 10 12 14
Quill v5.1 Unbounded Queue 9 9 11 11 12 14
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 v5.1 Bounded Dropping Queue 11 12 13 14 15 17
fmtlog 10 12 13 14 16 17
Quill v5.1 Unbounded Queue 14 15 16 16 18 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 v5.1 Bounded Dropping Queue 11 13 13 14 16 18
XTR 9 11 13 14 31 39
Quill v5.1 Unbounded Queue 14 16 16 16 19 21
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 v5.1 Bounded Dropping Queue 49 51 54 56 95 123
Quill v5.1 Unbounded Queue 50 52 55 56 59 63
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 v5.1 Bounded Dropping Queue 52 57 70 74 87 95
MS BinLog 66 68 71 74 87 295
Quill v5.1 Unbounded Queue 86 95 105 111 125 143
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.50 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_v5_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_v5_1_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"
#include "quill/std/Array.h"

#include <string>
#include <utility>

int main()
{
  // Backend  
  quill::BackendOptions backend_options;
  quill::Backend::start(backend_options);

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

  // A log message with number 123
  int a = 123;
  std::string l = "log";
  LOG_INFO(logger, "A {} message with number {}", l, a);

  // libfmt formatting language is supported 3.14e+00
  double pi = 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 second
    LOG_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}", "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.");
}

Output

example_output.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)

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

Meson

Using WrapDB

Meson's wrapdb includes a quill package, which repackages quill to be built by meson as a subproject.

Manual Integration

If you prefer not to use WrapDB, you can manually integrate Quill into your project by following these steps:

Integration Steps

Once the library is integrated into your Meson project, follow these steps to ensure proper usage:

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:

  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.