Home

Awesome

<div align="center"> <br> <img src="docs/logo.png" alt="logo" width="200" height="auto" /> <h1>Quill</h1> <p><b>Asynchronous Low Latency C++ Logging Library</b></p> <div> <a href="https://github.com/odygrd/quill/actions?query=workflow%3Alinux"> <img src="https://img.shields.io/github/actions/workflow/status/odygrd/quill/linux.yml?branch=master&label=linux&logo=linux&style=flat-square" alt="linux-ci" /> </a> <a href="https://github.com/odygrd/quill/actions?query=workflow%3Amacos"> <img src="https://img.shields.io/github/actions/workflow/status/odygrd/quill/macos.yml?branch=master&label=macos&logo=apple&logoColor=white&style=flat-square" alt="macos-ci" /> </a> <a href="https://github.com/odygrd/quill/actions?query=workflow%3Awindows"> <img src="https://img.shields.io/github/actions/workflow/status/odygrd/quill/windows.yml?branch=master&label=windows&logo=windows&logoColor=blue&style=flat-square" alt="windows-ci" /> </a> </div> <div> <a href="https://codecov.io/gh/odygrd/quill"> <img src="https://img.shields.io/codecov/c/gh/odygrd/quill/master.svg?logo=codecov&style=flat-square" alt="Codecov" /> </a> <a href="https://app.codacy.com/gh/odygrd/quill/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade"> <img src="https://img.shields.io/codacy/grade/cd387bc34658475d98bff84db3ad5287?logo=codacy&style=flat-square" alt="Codacy" /> </a> <a href="https://www.codefactor.io/repository/github/odygrd/quill"> <img src="https://img.shields.io/codefactor/grade/github/odygrd/quill?logo=codefactor&style=flat-square" alt="CodeFactor" /> </a> </div> <div> <a href="https://opensource.org/licenses/MIT"> <img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license" /> </a> <a href="https://en.wikipedia.org/wiki/C%2B%2B17"> <img src="https://img.shields.io/badge/language-C%2B%2B17-red.svg?style=flat-square" alt="language" /> </a> </div> <h4> <a href="https://quillcpp.readthedocs.io" title="Explore the full documentation">πŸ“š Documentation</a> <span> Β· </span> <a href="https://quillcpp.readthedocs.io/en/latest/cheat_sheet.html" title="Quick reference for common tasks">⚑ Cheat Sheet</a> <span> Β· </span> <a href="https://github.com/odygrd/quill/issues/new?assignees=&labels=&projects=&template=bug-report.md&title=" title="Report a bug or issue">πŸ› Report Bug</a> <span> Β· </span> <a href="https://github.com/odygrd/quill/issues/new?assignees=&labels=&projects=&template=feature_request.md&title=">πŸ’‘ Request Feature</a> </h4> <div align="center"><img src="docs/quill_demo.gif" width="75%" ></div> </div>

🧭 Table of Contents

✨ Introduction

Quill is a high-performance asynchronous logging library. It is particularly suited for performance-critical applications where every microsecond counts.

Try it on Compiler Explorer

⏩ Quick Start

Getting started is easy and straightforward. Follow these steps to integrate the library into your project:

Installation

You can install Quill using the package manager of your choice:

Package ManagerInstallation Command
vcpkgvcpkg install quill
Conanconan install quill
Homebrewbrew install quill
Meson WrapDBmeson wrap install quill
Condaconda install -c conda-forge quill
Bzlmodbazel_dep(name = "quill", version = "x.y.z")
xmakexrepo install quill
nixnix-shell -p quill-log

Setup

Once installed, you can start using Quill with the following code:

#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
#include "quill/sinks/ConsoleSink.h"
#include <string_view>

int main()
{
  quill::Backend::start();

  quill::Logger* logger = quill::Frontend::create_or_get_logger(
    "root", quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1"));

  LOG_INFO(logger, "Hello from {}!", std::string_view{"Quill"});
}

🎯 Features

πŸš€ Performance

System Configuration

Latency

The results presented in the tables below are measured in nanoseconds (ns).

The tables are sorted by the 95th percentile

Logging Numbers

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

1 Thread Logging
Library50th75th90th95th99th99.9th
Quill Bounded Dropping Queue7889911
fmtlog9910101213
Quill Unbounded Queue101010101214
PlatformLab NanoLog131416171925
MS BinLog212122225693
XTR7729303353
Reckless262831323549
Iyengar NanoLog8396117125152197
spdlog143147152158165177
g3log116112591329141916021827

numbers_1_thread_logging.webp

4 Threads Logging Simultaneously
Library50th75th90th95th99th99.9th
fmtlog899101113
Quill Bounded Dropping Queue8910101214
XTR789113138
Quill Unbounded Queue101111121315
PlatformLab NanoLog151720232732
Reckless192326283455
MS BinLog2122222362100
Iyengar NanoLog5890123131168242
spdlog210243288313382694
g3log127113371396143716141899

numbers_4_thread_logging.webp

Logging Large Strings

Logging std::string over 35 characters to prevent the short string optimization.

LOG_INFO(logger, "Logging int: {}, int: {}, string: {}", i, j, large_string).

1 Thread Logging
Library50th75th90th95th99th99.9th
Quill Bounded Dropping Queue111313141516
fmtlog111213141517
Quill Unbounded Queue141516171819
MS BinLog2223242561100
PlatformLab NanoLog151721273339
XTR8929313554
Reckless91107115118124135
Iyengar NanoLog8697119128159268
spdlog120124128132141151
g3log8819561018108912641494

large_strings_1_thread_logging.webp

4 Threads Logging Simultaneously
Library50th75th90th95th99th99.9th
XTR91113143240
fmtlog111213141619
Quill Bounded Dropping Queue131415161719
Quill Unbounded Queue151617181921
MS BinLog2325272865105
PlatformLab NanoLog162032384451
Reckless7994104107114132
Iyengar NanoLog8593125133168237
spdlog178218261281381651
g3log99210551121117813601600

large_strings_4_thread_logging.webp

Logging Complex Types

Logging std::vector<std::string> containing 16 large strings, each ranging from 50 to 60 characters.

LOG_INFO(logger, "Logging int: {}, int: {}, vector: {}", i, j, v).

1 Thread Logging
Library50th75th90th95th99th99.9th
Quill Bounded Dropping Queue485053555862
Quill Unbounded Queue545657586166
MS BinLog6869727479281
XTR284294340346356575
fmtlog711730754770804834
spdlog619162616330638666337320

vector_1_thread_logging.webp

4 Threads Logging Simultaneously
Library50th75th90th95th99th99.9th
Quill Bounded Dropping Queue505254566082
MS BinLog7072757988286
Quill Unbounded Queue97107116122135148
XTR512711761791865945
fmtlog780804823835860896
spdlog646965496641673576319430

vector_4_thread_logging.webp

The benchmark methodology involves 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 the frontend 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, BackendOptions::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 frontend 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

🧩 Usage

#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}", std::string_view {"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

External CMake

Building and Installing Quill

To get started with Quill, clone the repository and install it using CMake:

git clone http://github.com/odygrd/quill.git
mkdir cmake_build
cd cmake_build
cmake ..
make install

Next, add Quill to your project using find_package():

find_package(quill REQUIRED)
target_link_libraries(your_target PUBLIC quill::quill)

Sample Directory Structure

Organize your project directory like this:

my_project/
β”œβ”€β”€ CMakeLists.txt
β”œβ”€β”€ main.cpp

Sample CMakeLists.txt

Here’s a sample CMakeLists.txt to get you started:

# If Quill is in a non-standard directory, specify its path.
set(CMAKE_PREFIX_PATH /path/to/quill)

# Find and link the Quill library.
find_package(quill REQUIRED)
add_executable(example main.cpp)
target_link_libraries(example PUBLIC quill::quill)

Embedded CMake

For a more integrated approach, embed Quill directly into your project:

Sample Directory Structure

my_project/
β”œβ”€β”€ quill/            # Quill repo folder
β”œβ”€β”€ CMakeLists.txt
β”œβ”€β”€ main.cpp

Sample CMakeLists.txt

Use this CMakeLists.txt to include Quill directly:

cmake_minimum_required(VERSION 3.1.0)
project(my_project)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_subdirectory(quill)
add_executable(my_project main.cpp)
target_link_libraries(my_project PUBLIC quill::quill)

Android NDK

Building Quill for Android? Add this flag during configuration:

-DQUILL_NO_THREAD_NAME_SUPPORT:BOOL=ON

Meson

Using WrapDB

Easily integrate Quill with Meson’s wrapdb:

meson wrap install quill

Manual Integration

Copy the repository contents to your subprojects directory and add the following to your meson.build:

quill = subproject('quill')
quill_dep = quill.get_variable('quill_dep')
my_build_target = executable('name', 'main.cpp', dependencies : [quill_dep], install : true)

Bazel

Using Blzmod

Quill is available on BLZMOD for easy integration.

Manual Integration

For manual setup, add Quill to your BUILD.bazel file like this:

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

VariableDescription
timestampCurrent timestamp
Metadata*Pointer to metadata information
Logger*Pointer to the logger instance
DecodeFuncA 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

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

πŸ“ 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.