Quill - Asynchronous Low Latency Logging Library for C++

Quill is an open source, cross platform C++17 logging library designed for latency sensitive applications.

Github

Install

Package Managers

Homebrew

vcpkg

Conan

brew install quill

vcpkg install quill

quill/[>=1.2.3]

CMake-Integration

External

Building and Installing Quill as Static Library
git clone https://github.com/odygrd/quill.git
mkdir cmake_build
cd cmake_build
make install

Note: To install in custom directory invoke cmake with -DCMAKE_INSTALL_PREFIX=/quill/install-dir/

Building and Installing Quill as Static Library With External libfmt
cmake -DCMAKE_PREFIX_PATH=/my/fmt/fmt-config.cmake-directory/ -DQUILL_FMT_EXTERNAL=ON -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 PRIVATE quill::quill)

Embedded

To embed the library directly, copy the source 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
add_subdirectory(quill)
add_executable(my_project main.cpp)
target_link_libraries(my_project PRIVATE quill::quill)

Tutorial

Basic Example

#include "quill/Quill.h"

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

  // Get a pointer to the default logger
  quill::Logger* dl = quill::get_logger();

  LOG_INFO(dl, "Welcome to Quill!");
  LOG_ERROR(dl, "An error message with error code {}, error message {}", 123, "system_error");
}

In the above example a default logger to stdout is created with it’s name set to “root”.

The default logger can be accessed easily by calling Logger* quill::get_logger(). Any newly created logger inherits the properties of the default root logger. Log level is always set to quill::LogLeveL::Info by default.

Each quill::Logger contains single or multiple quill::Handler objects. The handler objects actually deliver the log message to their. Each handler contains a quill::PatternFormatter object which is responsible for the formatting of the message.

A single backend thread is checking for new log messages periodically. Starting the backend thread is the responsibility of the user. The backend thread will automatically stop at the end of main printing every message, as long as the application is terminated gracefully.

Use of macros is unavoidable in order to achieve better runtime performance. The static information of a log (such as format string, log level, location) is created in compile time. It is passed along with the type of each argument as a template parameter to a decoding function. A template instantiation per log statement is created.

Logging Macros

The following macros are provided for logging:

LOG_TRACE_L3(logger, log_message_format, args)
LOG_TRACE_L2(logger, log_message_format, args)
LOG_TRACE_L1(logger, log_message_format, args)
LOG_DEBUG(logger, log_message_format, args)
LOG_INFO(logger, log_message_format, args)
LOG_WARNING(logger, log_message_format, args)
LOG_ERROR(logger, log_message_format, args)
LOG_CRITICAL(logger, log_message_format, args)
LOG_BACKTRACE(logger, log_message_format, args)

Handlers

Handlers are the objects that actually write the log to their target.

A quill::Handler object is the base class for each different handler derived classes.

Each handler is responsible for outputting the log to a single target (e.g file, console, db), and owns a quill::PatternFormatter object which formats the messages to its destination.

Upon the handler creation, the handler object is registered and owned by a central manager object the quill::detail::HandlerCollection

For files, one handler is created per filename. For stdout and stderr a default handler for each one is always created during initialisation. It is possible for the user to create multiple stdout or stderr handles by providing a unique id per handle.

When creating a custom logger one or more handlers for this logger can be specified. This can only be done only the logger creation.

Sharing handlers between loggers

It is possible to share the same handle object between multiple logger objects. For example when all logger objects are writing to the same file. The following code is also thread-safe.

// The first time this function is called a file handler is created for this filename.
// Calling the function with the same filename will return the existing handler
std::shared_ptr<quill::Handler> file_handler = quill::file_handler(filename, "w");

// Create a logger using this handler
quill::Logger* logger_foo = quill::create_logger("logger_foo", file_handler);

// Because a handler already created for this filename a pointer to the existing handler is returned
std::shared_ptr<quill::Handler> file_handler_2 = quill::file_handler(filename, "w");

// Create a new logger using this handler
quill::Logger* logger_bar = quill::create_logger("logger_bar", file_handler_2);

Handler Types

ConsoleHandler

The ConsoleHandler class sends logging output to streams stdout or stderr. Printing colour codes to terminal or windows console is also supported.

std::shared_ptr<Handler> quill::stdout_handler(std::string const &stdout_handler_name = std::string{"stdout"}, ConsoleColours const &console_colours = ConsoleColours{})
Parameters:
  • stdout_handler_name – a custom name for stdout_handler. This is only useful if you want to have multiple formats in the stdout. See example_stdout_multiple_formatters.cpp example

  • console_colours – a console colours configuration class

Returns:

a handler to the standard output stream

std::shared_ptr<Handler> quill::stderr_handler(std::string const &stderr_handler_name = std::string{"stderr"})
Parameters:

stderr_handler_name – a custom name for stdout_handler. This is only useful if you want to have multiple formats in the stderr. See example_stdout_multiple_formatters.cpp example

Returns:

a handler to the standard error stream

Creating multiple ConsoleHandler objects

While when operating to files, only one handle object can be created per file name, this is not the case for stdout or stderr. It is possible to create multiple handlers to stdout or stderr by providing a unique name to each handler.

This is useful for when you want to have different loggers writing to stdout with different format.

// Get the stdout file handler, with a unique name
std::shared_ptr<quill::Handler> stdout_handler_1 = quill::stdout_handler("stdout_1");

stdout_handler_1->set_pattern(
  "%(ascii_time) [%(process)] [%(thread)] LOG_%(level_name) %(logger_name) - %(message)", // message format
  "%D %H:%M:%S.%Qms %z",     // timestamp format
  quill::Timezone::GmtTime); // timestamp's timezone

quill::Logger* logger_foo = quill::create_logger("logger_foo", stdout_handler_1);

// Get the stdout file handler, with another unique name
std::shared_ptr<quill::Handler> stdout_handler_2 = quill::stdout_handler("stdout_2");

stdout_handler_2->set_pattern("%(ascii_time) LOG_%(level_name) %(logger_name) - %(message)", // message format
                              "%D %H:%M:%S.%Qms %z",     // timestamp format
                              quill::Timezone::GmtTime); // timestamp's timezone

quill::Logger* logger_bar = quill::create_logger("logger_bar", stdout_handler_2);

FileHandler

std::shared_ptr<Handler> quill::file_handler(fs::path const &filename, FileHandlerConfig const &config = FileHandlerConfig{}, FileEventNotifier file_event_notifier = FileEventNotifier{})

Creates or returns an existing handler to a file. If the file is already opened the existing handler for this file is returned instead.

Note

It is possible to remove the file handler and close the associated file by removing all the loggers associated with this handler with quill::remove_logger()

Parameters:
  • filename – the name of the file

  • config – configuration for the file handler

  • file_event_notifier – a FileEventNotifier to get callbacks to file events such as before_open, after_open etc

Returns:

A handler to a file

Logging to file
int main()
{
  quill::start();

  quill::std::shared_ptr<Handler> file_handler = quill::file_handler(filename, "w");
  quill::Logger* l = quill::create_logger("logger", file_handler);

  LOG_INFO(l, "Hello World");
  LOG_INFO(quill::get_logger("logger"), "Hello World");
}

RotatingFileHandler

std::shared_ptr<Handler> quill::rotating_file_handler(fs::path const &base_filename, RotatingFileHandlerConfig const &config = RotatingFileHandlerConfig{}, FileEventNotifier file_event_notifier = FileEventNotifier{})

Creates a new instance of the RotatingFileHandler class. If the file is already opened the existing handler for this file is returned instead.

Note

It is possible to remove the file handler and close the associated file by removing all the loggers associated with this handler with quill::remove_logger()

Parameters:
  • base_filename – the base file name

  • config – configuration for the rotating file handler

  • file_event_notifier – a FileEventNotifier to get callbacks to file events such as before_open, after_open etc

Returns:

a pointer to a rotating file handler

Rotating log by size
// Start the backend logging thread
quill::start();

// Create a rotating file handler with a max file size per log file and maximum rotation up to 5 times
quill::std::shared_ptr<Handler> file_handler = quill::rotating_file_handler(base_filename, "w", 1024, 5);

// Create a logger using this handler
quill::Logger* logger_bar = quill::create_logger("rotating", file_handler);

for (uint32_t i = 0; i < 15; ++i)
{
  LOG_INFO(logger_bar, "Hello from {} {}", "rotating logger", i);
}

// Get an instance to the existing rotating file handler
quill::std::shared_ptr<Handler> file_handler = quill::rotating_file_handler(base_filename);

TimeRotatingFileHandler

Warning

doxygenfunction: Cannot find function “quill::time_rotating_file_handler” in doxygen xml output for project “Quill” from directory: build/xml

Daily log
// Start the backend logging thread
quill::start();

// Create a rotating file handler which rotates daily at 02:00
quill::std::shared_ptr<Handler> file_handler =
  quill::time_rotating_file_handler(filename, "w", "daily", 1, 10, Timezone::LocalTime, "02:00");

// Create a logger using this handler
quill::Logger* logger_bar = quill::create_logger("daily_logger", file_handler);

LOG_INFO(logger_bar, "Hello from {}", "daily logger");
Hourly log
// Start the backend logging thread
quill::start();

// Create a rotating file handler which rotates every one hour and keep maximum 24 files
quill::std::shared_ptr<Handler> file_handler =
  quill::time_rotating_file_handler(filename, "w", "H", 24, 10);

// Create a logger using this handler
quill::Logger* logger_bar = quill::create_logger("daily_logger", file_handler);

LOG_INFO(logger_bar, "Hello from {}", "daily logger");

JsonFileHandler

std::shared_ptr<Handler> quill::json_file_handler(fs::path const &filename, JsonFileHandlerConfig const &config = JsonFileHandlerConfig{}, FileEventNotifier file_event_notifier = FileEventNotifier{})

Creates a new instance of the JsonFileHandler. If the file is already opened the existing handler for this file is returned instead.

When the JsonFileHandler is used named arguments need to be passed as the format string to the loggers. See examples/example_json_structured_log.cpp

Note

It is possible to remove the file handler and close the associated file by removing all the loggers associated with this handler with quill::remove_logger()

Parameters:
  • filename – the name of the file

  • config – configuration for the json file handler

  • file_event_notifier – a FileEventNotifier to get callbacks to file events such as before_open, after_open etc

Returns:

a pointer to a json file handler

Json log
quill::Config cfg;

// use the json handler
quill::std::shared_ptr<Handler> json_handler =
  quill::json_file_handler("json_output.log", "w", quill::FilenameAppend::DateTime);

// Change how the date is formatted in the structured log.
// JsonFileHandler must always have an empty pattern "" as the first argument.
json_handler->set_pattern("", std::string{"%Y-%m-%d %H:%M:%S.%Qus"});

// set this handler as the default for any new logger we are creating
cfg.default_handlers.emplace_back(json_handler);

quill::configure(cfg);

// Start the logging backend thread
quill::start();

// log to the json file ONLY by using the default logger
quill::Logger* logger = quill::get_logger();
for (int i = 0; i < 2; ++i)
{
  LOG_INFO(logger, "{method} to {endpoint} took {elapsed} ms", "POST", "http://", 10 * i);
}

Filters

A Filter class that can be used for filtering log records in the backend working thread.

This is a simple way to ensure that a logger or handler will only output desired log messages.

One or several quill::FilterBase can be added to a quill::Handler instance using the void add_filter(std::unique_ptr<FilterBase> filter)() The handler stores all added filters in a vector. The final log message is logged if all filters of the handler return true.

Filtering per handler

The below example logs all WARNING and higher log level messages to console and all INFO and lower level messages to a file.

// Filter class for our file handler
class FileFilter : public quill::FilterBase
{
public:
  FileFilter() : quill::FilterBase("FileFilter"){};

  QUILL_NODISCARD bool filter(char const* thread_id, std::chrono::nanoseconds log_record_timestamp,
                              quill::detail::LogRecordMetadata const& metadata,
                              fmt::memory_buffer const& formatted_record) noexcept override
  {
    if (metadata.level() < quill::LogLevel::Warning)
    {
      return true;
    }
    return false;
  }
};

// Filter for the stdout handler
class StdoutFilter : public quill::FilterBase
{
public:
  StdoutFilter() : quill::FilterBase("StdoutFilter"){};

  QUILL_NODISCARD bool filter(char const* thread_id, std::chrono::nanoseconds log_record_timestamp,
                              quill::detail::LogRecordMetadata const& metadata,
                              fmt::memory_buffer const& formatted_record) noexcept override
  {
    if (metadata.level() >= quill::LogLevel::Warning)
    {
      return true;
    }
    return false;
  }
};

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

  // Get a handler to the file
  // The first time this function is called a file handler is created for this filename.
  // Calling the function with the same filename will return the existing handler
  quill::std::shared_ptr<Handler> file_handler = quill::file_handler("example_filters.log", "w");

  // Create and add the filter to our handler
  file_handler->add_filter(std::make_unique<FileFilter>());

  // Also create an stdout handler
  quill::std::shared_ptr<Handler> stdout_handler = quill::stdout_handler("stdout_1");

  // Create and add the filter to our handler
  stdout_handler->add_filter(std::make_unique<StdoutFilter>());

  // Create a logger using this handler
  quill::Logger* logger = quill::create_logger("logger", {file_handler, stdout_handler});

  // Change the LogLevel to print everything
  logger->set_log_level(quill::LogLevel::TraceL3);

  // Log any message ..
}

Formatters

The quill::PatternFormatter specifies the layout of log records in the final output.

Each quill::Handler object owns a PatternFormatter object. This means that each Handler can be customised to output in a different format.

Customising the format output only be done prior to the creation of the logger by calling inline void Handler::set_pattern(std::string const &format_pattern, std::string const &timestamp_format = std::string{"%H:%M:%S.%Qns"}, Timezone timezone = Timezone::LocalTime)().

The following format is used by default :

ascii_time [thread_id] filename:line level_name logger_name - message

If no custom format is set each newly created Handler uses the same formatting as the default logger.

The format output can be customised by providing a string of certain attributes.

Name

Format

Description

ascii_time

%(ascii_time)

Human-readable time when the LogRecord was created. By default this is of the form ‘2003-07-08 16:49:45.896’ (the numbers after the period are millisecond portion of the time).

filename

%(filename)

Filename portion of pathname.

function_name

%( function_name)

Name of function containing the logging call.

level_name

%(level_name)

Text logging level for the message (‘TRACEL3’, ‘TRACEL2’, ‘TRACEL1’, ‘DEBUG’, ‘INFO’, ‘WARNING’, ‘ERROR’, ‘CRITICAL’, ‘BACKTRACE’).

level_id

%(level_id)

Abbreviated level name (‘T3’, ‘T2’, ‘T1’, ‘D’, ‘I’, ‘W’, ‘E’, ‘C’, ‘BT’).

lineno

%(lineno)

Source line number where the logging call was issued (if available).

message

%(message)

The logged message, computed as msg % args. This is set when Formatter.format() is invoked.

logger_name

%(logger_name)

Name of the logger used to log the call.

pathname

%(pathname)

Full pathname of the source file where the logging call was issued (if available).

thread

%(thread)

Thread ID (if available).

thread name

%(thread_name)

Thread name if set. The name of the thread must be set prior to issuing any log statement on that thread.

process

%(process)

Process ID

Customising the timestamp

The timestamp is customisable by :

  • Format. Same format specifiers as strftime(...) format without the additional .Qms .Qus .Qns arguments.

  • Local timezone or GMT timezone. Local timezone is used by default.

  • Fractional second precision. Using the additional fractional second specifiers in the timestamp format string.

Specifier

Description

%Qms

Milliseconds

%Qus

Microseconds

%Qns

Nanoseconds

By default "%H:%M:%S.%Qns" is used.

Note

MinGW does not support all strftime(...) format specifiers and you might get a bad alloc if the format specifier is not supported

Setting a default formatter for logging to stdout

// Get the stdout file handler
quill::std::shared_ptr<Handler> console_handler = quill::stdout_handler();

// Set a custom formatter for this handler
console_handler->set_pattern("%(ascii_time) [%(process)] [%(thread)] %(logger_name) - %(message)", // format
                          "%D %H:%M:%S.%Qms %z",     // timestamp format
                          quill::Timezone::GmtTime); // timestamp's timezone

// Config using the custom ts class and the stdout handler
quill::Config cfg;
cfg.default_handlers.emplace_back(console_handler);
quill::configure(cfg);

// Start the backend logging thread
quill::start();

// Log using the default logger
LOG_INFO(quill::get_logger(), "The default logger is using a custom format");

// Obtain a new logger. Since no handlers were specified during the creation of the new logger. The new logger will use the default logger's handlers. In that case it will use the stdout_handler with the modified format.
quill::Logger* logger_foo = quill::create_logger("logger_foo");

LOG_INFO(logger_foo, "The new logger is using the custom format");

Setting a default formatter on a FileHandler

// Start the logging backend thread
quill::start();

// Calling the function with the same filename will return the existing handler
quill::std::shared_ptr<Handler> file_handler = quill::file_handler(filename, "w");

// Set a custom pattern to this file handler
file_handler->set_pattern("%(ascii_time) [%(process)] [%(thread)] %(logger_name) - %(message)", // format
                          "%D %H:%M:%S.%Qms %z",     // timestamp format
                          quill::Timezone::GmtTime); // timestamp's timezone

// Create a logger using this handler
quill::Logger* logger_foo = quill::create_logger("logger_foo", file_handler);

// Log using the logger
LOG_INFO(logger_foo, "Hello from {}", "library foo");

Logger

Quill creates a default quill::Logger with a stdout handler with name as root. The default logger can be accessed easily by calling Logger* quill::get_logger()

The default logger can be easily customised by replacing its instance with another logger. It is possible to change the handler of the default logger and the formatter of the default logger This however has to be done in the beginning before the logger is used.

Logger creation

New logger instances can be created by the user with the desired name, handlers and formatter. The logger object are never instantiated directly. Instead they first have to get created by calling

Based on the create function that was used the new logger might inherit all properties of the default logger or get created with it’s own custom properties.

Logger *quill::create_logger(std::string const &logger_name, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)

Creates a new Logger using the existing root logger’s handler and formatter pattern

Note

: If the user does not want to store the logger pointer, the same logger can be obtained later by calling get_logger(logger_name);

Parameters:
  • logger_name – The name of the logger to add

  • timestamp_clock_type – rdtsc, chrono or custom clock

  • timestamp_clock – custom user clock

Returns:

A pointer to a thread-safe Logger object

Warning

doxygenfunction: Unable to resolve function “quill::create_logger” with arguments (std::string const&, Handler*, std::optional<TimestampClockType>, std::optional<TimestampClock*>) in doxygen xml output for project “Quill” from directory: build/xml. Potential matches:

- Logger *create_logger(std::string const &logger_name, std::initializer_list<std::shared_ptr<Handler>> handlers, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)
- Logger *create_logger(std::string const &logger_name, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)
- Logger *create_logger(std::string const &logger_name, std::shared_ptr<Handler> &&handler, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)
- Logger *create_logger(std::string const &logger_name, std::vector<std::shared_ptr<Handler>> &&handlers, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)
Logger *quill::create_logger(std::string const &logger_name, std::initializer_list<std::shared_ptr<Handler>> handlers, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)

Creates a new Logger using the custom given handler.

A custom formatter pattern the pattern can be specified during the handler creation for each handler

Parameters:
  • logger_name – The name of the logger to add

  • handlers – An initializer list of pointers to handlers for this logger

  • timestamp_clock_type – rdtsc, chrono or custom clock

  • timestamp_clock – custom user clock

Returns:

A pointer to a thread-safe Logger object

Warning

doxygenfunction: Unable to resolve function “quill::create_logger” with arguments (std::string const&, std::vector<std::shared_ptr<Handler>> const&, std::optional<TimestampClockType>, std::optional<TimestampClock*>) in doxygen xml output for project “Quill” from directory: build/xml. Potential matches:

- Logger *create_logger(std::string const &logger_name, std::initializer_list<std::shared_ptr<Handler>> handlers, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)
- Logger *create_logger(std::string const &logger_name, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)
- Logger *create_logger(std::string const &logger_name, std::shared_ptr<Handler> &&handler, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)
- Logger *create_logger(std::string const &logger_name, std::vector<std::shared_ptr<Handler>> &&handlers, std::optional<TimestampClockType> timestamp_clock_type = std::nullopt, std::optional<TimestampClock*> timestamp_clock = std::nullopt)

Logger access

Logger *quill::get_logger(char const *logger_name = nullptr)

Returns an existing logger given the logger name or the root logger if no arguments logger_name is passed. This function is also thread safe.

It is safe calling create_logger(“my_logger) and get_logger(“my_logger”) in different threads but the user has to make sure that the call to create_logger has returned in thread A before calling get_logger in thread B

Warning

the logger MUST have been created first by a call to create_logger.

Note

: for efficiency prefer storing the returned Logger* when get_logger(”…”) is used. Multiple calls to get_logger(name) will slow your code down since it will first use a lock mutex lock and then perform a look up. The advise is to store a quill::Logger* and use that pointer directly, at least in code hot paths.

Note

: safe to call even before even calling quill:start() unlike using get_root_logger()

Throws:

when – the requested logger does not exist

Parameters:

logger_name – The name of the logger, or no argument for the root logger

Returns:

A pointer to a thread-safe Logger object

Logger *quill::get_root_logger() noexcept

Provides fast access to the root logger.

Note

: This function can be used in the hot path and is more efficient than calling get_logger(nullptr)

Warning

This should be used only after calling quill::start(); if you need the root logger earlier then call get_logger() instead

Returns:

pointer to the root logger

Create single handler logger

// Get a handler to a file
quill::std::shared_ptr<Handler> file_handler = quill::file_handler("example.log", "w");

// Create a logger using this handler
quill::Logger* logger_foo = quill::create_logger("logger_foo", file_handler);

LOG_INFO(logger_foo, "Hello from {}", "library foo");

Create multi handler logger

// Get a handler to a file
quill::std::shared_ptr<Handler> file_handler = quill::file_handler(filename, "w");

// Get a handler to stdout
quill::std::shared_ptr<Handler> stdout_handler = quill::stdout_handler();

// Create a logger using both handlers
quill::Logger* logger_foo = quill::create_logger("logger_foo", {file_handler, quill::stdout_handler()});

LOG_INFO(logger_foo, "Hello from {}", "library foo");

Avoiding the use of Logger objects

For some applications the use of the single root logger might be enough. In that case passing the logger everytime to the macro becomes inconvenient. The solution is to overwrite the quill macros with your own macros.

#define MY_LOG_INFO(fmt, ...) QUILL_LOG_INFO(quill::get_root_logger(), fmt, ##__VA_ARGS__)

Or you can simply define

QUILL_ROOT_LOGGER_ONLY
  #define QUILL_ROOT_LOGGER_ONLY
  #include "quill/Quill.h"

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

    // because we defined QUILL_ROOT_LOGGER_ONLY we do not have to pass a logger* anymore, the root logger is always used
    LOG_INFO("Hello {}", "world");
    LOG_ERROR("This is a log error example {}", 7);
}

Backtrace Logging

Backtrace logging enables log messages to be stored in a ring buffer and either

  • displayed later on demand or

  • when a high severity log message is logged

Backtrace logging needs to be enabled first on the instance of quill::Logger

inline void quill::Logger::init_backtrace(uint32_t capacity, LogLevel backtrace_flush_level = LogLevel::None)

Init a backtrace for this logger. Stores messages logged with LOG_BACKTRACE in a ring buffer messages and displays them later on demand.

Parameters:
  • capacity – The max number of messages to store in the backtrace

  • backtrace_flush_level – If this loggers logs any message higher or equal to this severity level the backtrace will also get flushed. Default level is None meaning the user has to call flush_backtrace explicitly

inline void quill::Logger::flush_backtrace()

Dump any stored backtrace messages

Note

Backtrace log messages store the original timestamp of the message. Since they are kept and flushed later the timestamp in the log file will be out of order

Store messages in the ring buffer and display them when LOG_ERROR is logged

// Loggers can store in a ring buffer messages with LOG_BACKTRACE and display later when e.g.
// a LOG_ERROR message was logged from this logger

quill::Logger* logger = quill::create_logger("example_1");

// Enable the backtrace with a max ring buffer size of 2 messages which will get flushed when
// a LOG_ERROR(...) or higher severity log message occurs via this logger.
// Backtrace has to be enabled only once in the beginning before calling LOG_BACKTRACE(...) for the first time.
logger->init_backtrace(2, quill::LogLevel::Error);

LOG_INFO(logger, "BEFORE backtrace Example {}", 1);
LOG_BACKTRACE(logger, "Backtrace log {}", 1);
LOG_BACKTRACE(logger, "Backtrace log {}", 2);
LOG_BACKTRACE(logger, "Backtrace log {}", 3);
LOG_BACKTRACE(logger, "Backtrace log {}", 4);

// Backtrace is not flushed yet as we requested to flush on errors
LOG_INFO(logger, "AFTER backtrace Example {}", 1);

// log message with severity error - This will also flush the backtrace which has 2 messages
LOG_ERROR(logger, "An error has happened, Backtrace is also flushed.");

// The backtrace is flushed again after LOG_ERROR but in this case it is empty
LOG_ERROR(logger, "An second error has happened, but backtrace is now empty.");

// Log more backtrace messages
LOG_BACKTRACE(logger, "Another Backtrace log {}", 1);
LOG_BACKTRACE(logger, "Another Backtrace log {}", 2);

// Nothing is logged at the moment
LOG_INFO(logger, "Another log info");

// Still nothing logged - the error message is on a different logger object
quill::Logger* logger_2 = quill::create_logger("example_1_1");
LOG_CRITICAL(logger_2, "A critical error from different logger.");

// The new backtrace is flushed again due to LOG_CRITICAL
LOG_CRITICAL(logger, "A critical error from the logger we had a backtrace.");
13:02:03.405589220 [196635] example_backtrace.cpp:18 LOG_INFO      example_1 - BEFORE backtrace Example 1
13:02:03.405617051 [196635] example_backtrace.cpp:30 LOG_INFO      example_1 - AFTER backtrace Example 1
13:02:03.405628045 [196635] example_backtrace.cpp:33 LOG_ERROR     example_1 - An error has happened, Backtrace is also flushed.
13:02:03.405608746 [196635] example_backtrace.cpp:26 LOG_BACKTRACE example_1 - Backtrace log 3
13:02:03.405612082 [196635] example_backtrace.cpp:27 LOG_BACKTRACE example_1 - Backtrace log 4
13:02:03.405648711 [196635] example_backtrace.cpp:36 LOG_ERROR     example_1 - An second error has happened, but backtrace is now empty.
13:02:03.405662233 [196635] example_backtrace.cpp:43 LOG_INFO      example_1 - No errors so far
13:02:03.405694451 [196635] example_backtrace.cpp:47 LOG_CRITICAL  example_1_1 - A critical error from different logger.
13:02:03.405698838 [196635] example_backtrace.cpp:50 LOG_CRITICAL  example_1 - A critical error from the logger we had a backtrace.

Store messages in the ring buffer and display them on demand

quill::Logger* logger = quill::create_logger("example_2");

// Store maximum of two log messages. By default they will never be flushed since no LogLevel severity is specified
logger->init_backtrace(2);

LOG_INFO(logger, "BEFORE backtrace Example {}", 2);

LOG_BACKTRACE(logger, "Backtrace log {}", 100);
LOG_BACKTRACE(logger, "Backtrace log {}", 200);
LOG_BACKTRACE(logger, "Backtrace log {}", 300);

LOG_INFO(logger, "AFTER backtrace Example {}", 2);

// an error has happened - flush the backtrace manually
logger->flush_backtrace();

User Defined Types

Quill does asynchronous logging. When a user defined type has to be logged, the copy constructor is called and the formatting is performed on a backend logging thread via a call to ostream& operator<<(ostream& os, T const& t)()

This creates issues with user defined types that contain mutable references, raw pointers that can be mutated or for example a std::shared_ptr() that can be modified.

By default a compile time check is performed that checks for unsafe to copy types.

Many user defined types including STL containers, tuples and pairs of build in types are automatically detected in compile type as safe to copy and will pass the check.

The following code gives a good idea of the types that by default are safe to get copied

struct filter_copyable : std::disjunction<std::is_arithmetic<T>,
                                     is_string<T>,
                                     std::is_trivial<T>,
                                     is_user_defined_copyable<T>,
                                     is_user_registered_copyable<T>,
                                     is_copyable_pair<T>,
                                     is_copyable_tuple<T>,
                                     is_copyable_container<T>
                                     >

The following types is just a small example of detectable safe-to-copy types

std::vector<std::vector<std::vector<int>>>;
std::tuple<int,bool,double,float>>;
std::pair<char, double>;
std::tuple<std::vector<std::string>, std::map<int, std::sting>>;

Note

Passing pointers for logging is not permitted by libfmt in compile time, with the only exception being void*. Therefore they are excluded from the above check.

Requirements

To log a user defined type the following requirements must met:

  • The type has to be copy constructible

  • Specialize fmt::formatter<T> and implement parse and format methods (see here) or provide an overloaded insertion operator (see here)

Logging user defined types in default mode

In default mode copying non-trivial user defined types is not permitted unless they are tagged as safe to copy

Consider the following example :

 class User
 {
 public:
   User(std::string name) : name(std::move(name)){};

   friend std::ostream& operator<<(std::ostream& os, User2 const& obj)
   {
     os << "name : " << obj.name;
     return os;
   }
 private:
   std::string name;
 };

int main()
{
  User user{"Hello"};
  LOG_INFO(quill::get_logger(), "The user is {}", usr);
}

The above log statement would fail with a compiler error. The type is non-trivial, there is no way to automatically detect the type is safe to copy.

To log this user defined type we have two options:
  • call operator<<() on the caller hot path and pass a std::string() to the logger if the type contains mutable references and is not safe to copy

  • mark the type as safe to copy and let the backend logger thread do the formatting if the type is safe to copy

Registering or tagging user defined types as safe to copy

It is possible to mark the class as safe to copy and the logger will attempt to copy it. In this case the user defined type will get copied.

Note

It is the responsibility of the user to ensure that the class does not contain mutable references or pointers before tagging it as safe

There are 2 different ways to do that :

  1. Specialize copy_loggable

class User
{
public:
  User(std::string name) : name(std::move(name)){};

  friend std::ostream& operator<<(std::ostream& os, User2 const& obj)
  {
    os << "name : " << obj.name;
    return os;
  }

private:
  std::string name;
};

/** Registered as safe to copy **/
namespace quill {
  template <>
  struct copy_loggable<User> : std::true_type { };
}

int main()
{
  User user{"Hello"};
  LOG_INFO(quill::get_logger(), "The user is {}", usr);
}
  1. Use .. c:macro:: QUILL_COPY_LOGGABLE macro inside your class definition. This is not preferable as you need to edit the class to provide that

class User
{
public:
  User(std::string name) : name(std::move(name)){};

  friend std::ostream& operator<<(std::ostream& os, User2 const& obj)
  {
    os << "name : " << obj.name;
    return os;
  }

  QUILL_COPY_LOGGABLE; /** Tagged as safe to copy **/

private:
  std::string name;
};

int main()
{
  User user{"Hello"};
  LOG_INFO(quill::get_logger(), "The user is {}", usr);
}

Then the following will compile, the user defined type will get copied, and ostream& operator<<(ostream& os, T const& t)() will be called in the background thread.

Generally speaking, tagging functionality in this mode exists to also make the user thinks about the user defined type they are logging. It has to be maintained when a new class member is added. If the log level severity of the log statement is below INFO you might as well consider formatting the type to a string in the caller path instead of maintaining a safe-to-copy tag.

Logging non-copy constructible or unsafe to copy user defined types

Consider the following unsafe to copy user defined type. In this case we want to format on the caller thread.

This has to be explicitly done by the user as it might be expensive.

There is a utility function offered or users can write their own routine.

template<typename T>
std::string quill::utility::to_string(T const &obj) noexcept

By default the logger will take a copy of the passed object and then will call the operator<< in the background thread Use this function when : 1) [to print a non copyable] It is not possible to take a copy of an object when the object is not copyable 2) [to avoid race condition] You want to log a class that contains a reference or a pointer to another object as a class member, that can be updated before the logger thread calls operator<<. In that case when the logger thread tries to call operator<< on the class itself but the internal reference object might have changed between the time you wanted to log and when the logged thread called operator <<. Therefore, we need to accept the performance penalty calling operator<< on the caller thread 3) Similar to 2) but the class contains a class that contains a shared_pointer or a raw pointer which gets modified by the caller thread immediately after the logging call before the backend thread logs requires the custom type to have an operator<< overload defined

Parameters:

obj – the given object

Returns:

output of object’s operator<< as std::string

#include "quill/Quill.h"
#include "quill/Utility.h"

class User
{
public:
  User(std::string* name) : name(name){};

  friend std::ostream& operator<<(std::ostream& os, User const& obj)
  {
    os << "name : " << obj.name;
    return os;
  }

private:
  std::string* name;
};

int main()
{
  auto str = std::make_unique<std::string>("User A");
  User usr{str.get()};

  // We format the object in the hot path because it is not safe to copy this kind of object
  LOG_INFO(quill::get_logger(), "The user is {}", quill::utility::to_string(usr));

  // std::string* is modified - Here the backend worker receives a copy of User but the pointer to
  // std::string* is still shared and mutated in the below line
  str->replace(0, 1, "T");
}

Logging in QUIL_MODE_UNSAFE

When QUIL_MODE_UNSAFE is enabled, Quill will not check in compile time for safe to copy user defined types.

All types will are copied unconditionally in this mode as long as they are copy constructible. This mode is not recommended as the user has to be extremely careful about any user define type they are logging.

However, it is there for users who don’t want to tag their types.

The following example compiles and copies the user defined type even tho it is a non-trivial type.

#define QUIL_MODE_UNSAFE
#include "quill/Quill.h"

class User
{
public:
  User(std::string name) : name(std::move(name)){};

  friend std::ostream& operator<<(std::ostream& os, User2 const& obj)
  {
    os << "name : " << obj.name;
    return os;
  }
private:
  std::string name;
};

int main()
{
  User user{"Hello"};
  LOG_INFO(quill::get_logger(), "The user is {}", usr);
}

Features

Thread Safety

All components and API offered to the user is intended to be thread-safe without any special work needing to be done.

quill::Logger are thread safe by default. The same instance can be used to log by any thread. Any thread can safely modify the active log level of the logger.

Logging a non copyable, non movable user defined type

Quill will copy all arguments passed as arguments and perform all the formatting in the background thread. Therefore, the arguments passed to the logger needs to be copyable or at least movable. If the argument can not be made copyable then the user has to convert it to a string first before passing it to the logger.

Guaranteed logging

Quill uses a thread-local single-producer-single-consumer queue to forward logs records to the backend thread. By default an unbounded queue is used with an initially small size for performance reasons. If the queue becomes full the user can suffer a small performance penalty as a new queue will get allocated. Log messages are never dropped.

Customising the queue size

The queue size if configurable in runtime in quill::Config and applies to both bounded and unbounded queues.

Enabling non-guaranteed logging mode

If this option is enabled in TweakMe.h then the queue will never re-allocate but log messages will be dropped instead. If any messages are dropped then the user is notified by logging the number of dropped messages to stderr

Flush Policy and Force Flushing

By default quill lets libc to flush whenever it sees fit in order to achieve good performance. You can explicitly instruct the logger to flush all its contents. The logger will in turn flush all existing handlers.

Note

The thread that calls :quill::flush() will block until every message up to that point is flushed.

inline void quill::flush()

Blocks the caller thread until all log messages up to the current timestamp are flushed

The backend thread will call write on all handlers for all loggers up to the point (timestamp) that this function was called.

Note

This function will not do anything if called while the backend worker is not running

Application Crash Policy

When the program is terminated gracefully, quill will go through its destructor where all messages are guaranteed to be logged.

However, if the applications crashes, log messages can be lost.

To avoid losing messages when the application crashes due to a signal interrupt the user must setup it’s own signal handler and call quill::flush() inside the signal handler.

There is a built-in signal handler that offers this crash-safe behaviour and can be enabled in quill::start()

Log Messages Timestamp Order

Quill creates a single worker backend thread which orders the messages in all queues by timestamp before printing them to the log file.

Number of Backend Threads

Quill focus is on low latency and not high throughput. Therefore, there is only one backend thread that will process all logs.

Latency of the first log message

A queue and an internal buffer will be allocated on the first log message of each thread. If the latency of the first log message is important it is recommended to call quill::preallocate()

inline void quill::preallocate()

Pre-allocates the thread-local data needed for the current thread. Although optional, it is recommended to invoke this function during the thread initialisation phase before the first log message.

Configuration

Quill offers a few customisation options which are also very well documented.

Have a look at files Config.h under the namespace quill::config().

Ideally each hot thread runs on an isolated CPU. Then the backend logging thread should also be pinned to an either isolated or a junk CPU core.

Also the file TweakMe.h offers some compile time customisations. In release builds lower severity log levels such as LOG_TRACE or LOG_DEBUG statements can be compiled out to reduce the number of branches in the application. This can be done by editing TweakMe.h or invoking cmake

cmake .. -DCMAKE_CXX_FLAGS="-DQUILL_ACTIVE_LOG_LEVEL=QUILL_LOG_LEVEL_INFO"

Usage

Quickstart

#include "quill/Quill.h"

int main()
{
  // optional configuration before calling quill::start()
  quill::Config cfg;
  cfg.enable_console_colours = true;
  quill::configure(cfg);

  // starts the logging thread
  quill::start();

  // creates a logger
  quill::Logger* logger = quill::get_logger("my_logger");

  // log
  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.");
}

Single logger object

// When QUILL_ROOT_LOGGER_ONLY is defined then only a single root logger object is used
#define QUILL_ROOT_LOGGER_ONLY

#include "quill/Quill.h"

int main()
{
  // quill::Handler* handler = quill::stdout_handler(); /** for stdout **/
  quill::Handler* handler = quill::file_handler("quickstart.log", "w");
  handler->set_pattern("%(ascii_time) [%(thread)] %(fileline:<28) LOG_%(level_name) %(message)");

  // set configuration
  quill::Config cfg;
  cfg.default_handlers.push_back(handler);

  // Apply configuration and start the backend worker thread
  quill::configure(cfg);
  quill::start();

  LOG_INFO("Hello {}", "world");
  LOG_ERROR("This is a log error example {}", 7);
}

Log to file

#include "quill/Quill.h"

int main()
{
  quill::Handler* handler = quill::file_handler("quickstart.log", "w");
  handler->set_pattern("%(ascii_time) [%(thread)] %(fileline:<28) %(level_name) %(logger_name:<12) %(message)");

  // set configuration
  quill::Config cfg;
  cfg.default_handlers.push_back(handler);

  // Apply configuration and start the backend worker thread
  quill::configure(cfg);
  quill::start();

  auto my_logger = quill::create_logger("mylogger");
  my_logger->set_log_level(quill::LogLevel::Debug);

  LOG_INFO(my_logger, "Hello {}", "world");
  LOG_ERROR(my_logger, "This is a log error example {}", 7);
}

User’s API

Config Class

struct Config

Public Members

std::string backend_thread_name = "Quill_Backend"

Custom name for the backend thread.

bool backend_thread_yield = false

Determines whether the backend thread will “busy wait” by spinning around every caller thread’s local spsc queue. If enabled, this option reduces the OS scheduler priority when the backend worker thread is running on a shared CPU. The thread will yield when there is no remaining work to do.

Note

This option only takes effect when backend_thread_sleep_duration is set to 0.

std::chrono::nanoseconds backend_thread_sleep_duration = std::chrono::nanoseconds{500}

Determines the duration for which the backend thread will “busy wait” by spinning around every caller thread’s local spsc queue. If a value is set, each time the backend thread sees that there are no remaining logs to process in the queues, it will sleep for the specified duration.

size_t backend_thread_use_transit_buffer = true

Determines the behavior of the backend worker thread. By default, it will drain all hot queues and buffer the messages. If this option is set to false, the backend thread will simply process the message with the lowest timestamp from the SPSC queues without buffering.

Note

It is generally not recommended to set this to false, unless you want to limit the logging thread’s memory usage.

size_t backend_thread_transit_events_soft_limit = 800

The backend worker thread gives priority to reading messages from the SPSC queues of all the hot threads and temporarily buffers them.

If the hot threads continuously push messages to the queues (e.g., logging in a loop), no logs can ever be processed.

When the soft limit is reached (default: 800), this number of events will be logged to the log files before continuing to read the SPSC queues.

The SPSC queues are emptied on each iteration, so the actual messages from the SPSC queues can be much greater than the backend_thread_transit_events_soft_limit.

Note

This number represents a limit across ALL hot threads.

Note

Applicable only when backend_thread_use_transit_buffer = true.

size_t backend_thread_transit_events_hard_limit = 100'000

The backend worker thread gives priority to reading messages from the SPSC queues of all the hot threads and temporarily buffers them.

If the hot threads continuously push messages to the queues (e.g., logging in a loop), no logs can ever be processed.

As the backend thread buffers messages, it can keep buffering indefinitely if the hot threads keep pushing.

This limit is the maximum size of the backend thread buffer. When reached, the backend worker thread will stop reading the SPSC queues until there is space available in the buffer.

Note

This limit applies PER hot thread.

Note

Applicable only when backend_thread_use_transit_buffer = true.

uint32_t backend_thread_initial_transit_event_buffer_capacity = 64

The backend worker thread pops all log messages from the SPSC queues and buffers them in a local ring buffer queue as transit events. The transit_event_buffer is unbounded, with a customizable initial capacity (in items, not bytes). Each newly spawned hot thread will have its own transit_event_buffer. The capacity must be a power of two.

Note

Applicable only when backend_thread_use_transit_buffer = true.

bool backend_thread_strict_log_timestamp_order = true

The backend worker thread iterates through all active SPSC queues and pops all messages from each queue. It then sorts the messages by timestamp and logs them.

Each active queue corresponds to a thread, and when multiple threads are logging simultaneously, it is possible to read a timestamp from the last queue in the iteration but miss that timestamp when the first queue was read because it was not available at that time.

When this option is enabled, the backend worker thread takes a timestamp (now()) before reading the queues. It uses that timestamp to ensure that each log message’s timestamp from the active queues is less than or equal to the stored now() timestamp, guaranteeing ordering by timestamp.

Messages that fail the above check are not logged and remain in the queue. They are checked again in the next iteration. The timestamp check is performed with microsecond precision.

Enabling this option may cause a delay in popping messages from the SPSC queues.

Note

Applicable only when backend_thread_use_transit_buffer = true.

bool backend_thread_empty_all_queues_before_exit = true

When this option is enabled and the application is terminating, the backend worker thread will not exit until all the SPSC queues are empty. This ensures that all messages are logged.

However, if there is a thread during application destruction that keeps trying to log indefinitely, the backend worker thread will be unable to exit because it keeps popping log messages.

When this option is disabled, the backend worker thread will try to read the queues once and then exit. Reading the queues only once means that some log messages can be dropped, especially when backend_thread_strict_log_timestamp_order is set to true.

uint16_t backend_thread_cpu_affinity = (std::numeric_limits<uint16_t>::max)()

Pins the backend thread to the specified CPU.

By default, Quill does not pin the backend thread to any CPU unless a value is specified. Use std::numeric_limits<uint16_t>::max() as an undefined value to avoid setting CPU affinity.

std::string default_logger_name = "root"

Sets the name of the root logger.

backend_worker_notification_handler_t backend_thread_notification_handler

The background thread might occasionally throw an exception that cannot be caught in the user threads. In that case, the backend worker thread will call this callback instead.

Set up a custom notification handler to be used if the backend thread encounters any error. This handler is also used to deliver messages to the user, such as when the unbounded queue reallocates or when the bounded queue becomes full.

When not set here, the default is: backend_thread_notification_handler = [](std::string const& s) { std::cerr << s << std::endl; }

To disable notifications, use: backend_thread_notification_handler = [](std::string const&) { }

TimestampClock *default_custom_timestamp_clock = nullptr

Sets a custom clock that will be used to obtain the timestamp. This is useful, for example, during simulations where you need to simulate time.

TimestampClockType default_timestamp_clock_type = TimestampClockType::Tsc

Sets the clock type that will be used to obtain the timestamp. Options: rdtsc or system clock.

  • rdtsc mode: TSC clock provides better performance on the caller thread. However, the initialization time of the application is longer as multiple samples need to be taken in the beginning to convert TSC to nanoseconds.

    When using the TSC counter, the backend thread will periodically call std::chrono::system_clock::now() to resync the TSC based on the system clock. The backend thread constantly keeps track of the difference between TSC and the system wall clock to provide accurate timestamps.

  • system mode: std::chrono::system_clock::now() is used to obtain the timestamp.

By default, rdtsc mode is enabled.

Note

You need to have an invariant TSC for this mode to work correctly. Otherwise, use TimestampClockType::System.

std::vector<std::shared_ptr<Handler>> default_handlers = {}

Resets the root logger and recreates the logger with the given handler. This function can also be used to change the format pattern of the logger. If the vector is empty, the stdout handler is used by default.

bool enable_console_colours = false

Enables colors in the terminal. This option is only applicable when the default_handlers vector is empty (the default stdout handler is used). If you set up your own stdout handler with a custom pattern, you need to enable the colors yourself. See example_console_colours_with_custom_formatter.cpp and example_console_colours.cpp for examples.

std::chrono::milliseconds rdtsc_resync_interval = std::chrono::milliseconds{500}

This option is only applicable if the RDTSC clock is enabled. When the system clock is used, this option can be ignored.

Controls the frequency at which the backend thread recalculates and syncs the TSC by obtaining the system time from the system wall clock. The TSC clock drifts slightly over time and is not synchronized with NTP server updates. A smaller value results in more accurate log timestamps. Decreasing this value further provides more accurate timestamps with the system_clock. Changing this value only affects the performance of the backend worker thread.

uint32_t default_queue_capacity = {131'072}

Quill uses an unbounded/bounded SPSC queue per spawned thread to forward the log messages to the backend thread. During high logging activity, if the backend thread cannot consume logs fast enough, the queue may become full. In this scenario, the caller thread does not block but instead allocates a new queue with the same capacity. If the backend thread is falling behind, consider reducing the sleep duration of the backend thread or pinning it to a dedicated core to keep the queue less congested. The queue size can be increased or decreased based on user needs. The queue is shared between two threads and should not exceed the size of the LLC cache.

Note

This capacity automatically doubles when the unbounded queue is full.

Warning

The configured queue size must be in bytes, a power of two, and a multiple of the page size (4096). For example: 32,768; 65,536; 131,072; 262,144; 524,288.

bool enable_huge_pages_hot_path = {false}

When set to true, enables huge pages for all queue allocations on the hot path. Make sure you have huge pages enabled on your Linux system for this to work.

To check if huge pages are enabled: cat /proc/meminfo | grep HugePages

To set the number of huge pages: sudo sysctl -w vm.nr_hugepages=<number_of_hugepages>

Note

This option is only supported on Linux.

Log Levels

enum quill::LogLevel

Log level enum

Values:

enumerator TraceL3
enumerator TraceL2
enumerator TraceL1
enumerator Debug
enumerator Info
enumerator Warning
enumerator Error
enumerator Critical
enumerator Backtrace

This is only used for backtrace logging. Should not be set by the user.

enumerator None
enumerator Dynamic

This is only used for dynamic logging. Should not be set by the user.

Logger Class

class Logger

Thread safe logger. Logger must be obtained from LoggerCollection get_logger(), therefore constructors are private

Public Functions

Logger(Logger const&) = delete

Deleted

inline void *operator new(size_t i)

We align the logger object to it’s own cache line. It shouldn’t make much difference as the logger object size is exactly 1 cache line

inline LogLevel log_level() const noexcept
Returns:

The log level of the logger

inline void set_log_level(LogLevel log_level)

Set the log level of the logger

Parameters:

log_level – The new log level

template<LogLevel log_statement_level>
inline bool should_log() const noexcept

Checks if the given log_statement_level can be logged by this logger

Template Parameters:

log_statement_level – The log level of the log statement to be logged

Returns:

bool if a message can be logged based on the current log level

inline bool should_log(LogLevel log_statement_level) const noexcept

Checks if the given log_statement_level can be logged by this logger

Parameters:

log_statement_level – The log level of the log statement to be logged

Returns:

bool if a message can be logged based on the current log level

template<typename TMacroMetadata, typename TFormatString, typename ...FmtArgs>
inline void log(LogLevel dynamic_log_level, TFormatString format_string, FmtArgs&&... fmt_args)

Push a log message to the spsc queue to be logged by the backend thread. One spsc queue per caller thread. This function is enabled only when all arguments are fundamental types. This is the fastest way possible to log

Note

This function is thread-safe.

Parameters:
  • format_string – format

  • fmt_args – arguments

inline void init_backtrace(uint32_t capacity, LogLevel backtrace_flush_level = LogLevel::None)

Init a backtrace for this logger. Stores messages logged with LOG_BACKTRACE in a ring buffer messages and displays them later on demand.

Parameters:
  • capacity – The max number of messages to store in the backtrace

  • backtrace_flush_level – If this loggers logs any message higher or equal to this severity level the backtrace will also get flushed. Default level is None meaning the user has to call flush_backtrace explicitly

inline void flush_backtrace()

Dump any stored backtrace messages

Handler Base Class

class Handler

Base class for handlers

Subclassed by quill::NullHandler, quill::StreamHandler

Public Functions

Handler() = default

Constructor Uses the default pattern formatter

virtual ~Handler() = default

Destructor

inline void set_pattern(std::string const &log_pattern, std::string const &time_format = std::string{"%H:%M:%S.%Qns"}, Timezone timezone = Timezone::LocalTime)

Set a custom formatter for this handler

Warning

This function is not thread safe and should be called before any logging to this handler happens Prefer to set the formatter in the constructor when possible via the FileHandlerConfig

Parameters:
  • log_pattern – format pattern see PatternFormatter

  • time_format – defaults to “%H:%M:%S.%Qns”

  • timezone – defaults to PatternFormatter::Timezone::LocalTime

inline PatternFormatter &formatter()

Returns the owned formatter by the handler

Note

: Accessor for backend processing

Returns:

reference to the pattern formatter of this handler

virtual void write(fmt_buffer_t const &formatted_log_message, quill::TransitEvent const &log_event) = 0

Logs a formatted log message to the handler

Note

: Accessor for backend processing

Parameters:
  • formatted_log_message – input log message to write

  • log_event – transit event

virtual void flush() noexcept = 0

Flush the handler synchronising the associated handler with its controlled output sequence.

inline virtual void run_loop() noexcept

Executes periodically by the backend thread, providing an opportunity for the user to perform custom tasks. For example, batch committing to a database, or any other desired periodic operations.

Note

It is recommended to avoid performing heavy operations within this function as it may adversely affect the performance of the backend thread.

inline void set_log_level(LogLevel log_level)

Sets a log level filter on the handler. Log statements with higher or equal severity only will be logged

Note

thread safe

Parameters:

log_level – the log level severity

inline LogLevel get_log_level() const noexcept

Looks up the existing log level filter that was set by set_log_level and returns the current log level

Note

thread-safe

Returns:

the current log level of the log level filter

void add_filter(std::unique_ptr<FilterBase> filter)

Filters Adds a new filter for this handler. Filters can be added at any time to the handler.

Note

: thread-safe

Parameters:

filter – instance of a filter class as unique ptr

bool apply_filters(char const *thread_id, std::chrono::nanoseconds log_message_timestamp, LogLevel log_level, MacroMetadata const &metadata, fmt_buffer_t const &formatted_record)

Apply all registered filters.

Note

: called internally by the backend worker thread.

Returns:

result of all filters

Filter Base Class

class FilterBase

Base filter class. Filters can be added to Handlers

Public Functions

inline explicit FilterBase(std::string filter_name)

Constructor

Parameters:

filter_name – unique filter name

virtual ~FilterBase() = default

Destructor

virtual bool filter(char const *thread_id, std::chrono::nanoseconds log_message_timestamp, MacroMetadata const &metadata, fmt_buffer_t const &formatted_record) noexcept = 0

Filters a log message

Parameters:
  • thread_id – thread id

  • log_message_timestamp – timestamp

  • metadata – log message

  • formatted_record – formatted log message

Returns:

true if the log message should be written to the file, false otherwise

inline virtual std::string const &get_filter_name() const noexcept

Gets the name of the filter. Only useful if an existing filter is needed to be looked up

Returns:

the name of the filter

PatternFormatter Class

class PatternFormatter

Public Types

enum TimestampPrecision

Public classes Stores the precision of the timestamp

Values:

enumerator None
enumerator MilliSeconds
enumerator MicroSeconds
enumerator NanoSeconds

Public Functions

inline PatternFormatter()

Main PatternFormatter class Constructor

inline PatternFormatter(std::string const &format_pattern, std::string const &timestamp_format, Timezone timezone)

Constructor for a PatterFormatter with a custom format

Parameters:
  • format_pattern – format_pattern a format string. Must be passed using the macro QUILL_STRING(“format string”);

  • timestamp_format – The for format of the date. Same as strftime() format with extra specifiers Qms Qus Qns

  • timezone – The timezone of the timestamp, local_time or gmt_time

~PatternFormatter() = default

Destructor

Internal Classes

class HandlerCollection

Creates and manages active handlers

Public Functions

HandlerCollection(HandlerCollection const&) = delete

Deleted

std::shared_ptr<Handler> stdout_console_handler(std::string const &stdout_handler_name = std::string{"stdout"}, ConsoleColours const &console_colours = ConsoleColours{})

The handlers are used by the backend thread, so after their creation we want to avoid mutating their member variables. So here the API returns pointers to the base class to somehow restrict the user from creating a handler and calling a set() function on the handler after it’s creation. Currently no built-in handlers have setters function.

template<typename THandler, typename ...Args>
inline std::shared_ptr<Handler> create_handler(std::string const &handler_name, Args&&... args)

Create a handler

std::shared_ptr<Handler> get_handler(std::string const &handler_name)

Get an existing a handler

Parameters:

handler_name – the name of the handler

Throws:

std::runtime_error – if the handler does not exist

Returns:

a shared_ptr to the handler

void subscribe_handler(std::shared_ptr<Handler> const &handler_to_insert)

Subscribe a handler to the vector of active handlers so that the backend thread can see it Called each time a new Logger instance is created. If the Handler already exists then it is not added in the collection again Objects that are added to _active_handlers_collection never get removed again

Parameters:

handler_to_insert – a handler to add

std::vector<std::weak_ptr<Handler>> active_handlers() const

Get a list of all the active subscribed handlers. The list contains each handler only once regardless the amount of Logger instances using it This is not used for logging by the backend but only in special cases when e.g. it needs to iterate through all handlers for e.g. to flush

Returns:

a vector containing all the active handlers

void remove_unused_handlers()

Called by the backend worker thread only to remove any handlers that are not longer in use