XUtils

Quill

Asynchronous cross platform low latency logging library. [MIT]


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:

  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


Articles

  • coming soon...