Quill - Asynchronous Low Latency Logging Library for C++
Quill is an open source, cross platform C++17 logging library designed for latency sensitive applications.
Install
Package Managers
Homebrew |
vcpkg |
Conan |
---|---|---|
|
|
|
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.
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 ×tamp_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)
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 aquill::Logger*
and use that pointer directly, at least in code hot paths.Note
: safe to call even before even calling
quill:start()
unlike usingget_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
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:
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 astd::string()
to the logger if the type contains mutable references and is not safe to copymark 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 :
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);
}
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 storednow()
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.
-
std::string backend_thread_name = "Quill_Backend"
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.
-
enumerator TraceL3
Logger Class
-
class Logger
Thread safe logger. Logger must be obtained from LoggerCollection get_logger(), therefore constructors are private
Public Functions
-
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 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
-
inline void *operator new(size_t i)
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
-
Handler() = default
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
-
inline explicit FilterBase(std::string filter_name)
PatternFormatter Class
-
class PatternFormatter
Public Types
Public Functions
-
inline PatternFormatter()
Main PatternFormatter class Constructor
-
inline PatternFormatter(std::string const &format_pattern, std::string const ×tamp_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
-
inline PatternFormatter()
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.
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
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
-
HandlerCollection(HandlerCollection const&) = delete