A high-performance, cross-platform header-only logging library for C++20 using a multi-producer, multi-consumer ring buffer with multi-sink support, source-location logging, and log rotation capabilities.
- High Performance: Asynchronous logging using the slick-queue ring buffer for minimal latency
- Modern Formatting: Uses C++20
std::formatfor type-aware, efficient string formatting - Multi-Sink Architecture: Log to multiple destinations simultaneously (console, files, custom sinks)
- Log Rotation: Size-based and time-based rotation with configurable retention
- Colored Console Output: ANSI color support with configurable error routing
- Source Locations by Default:
LOG_*macros include the call-site file and line, with runtime controls for basename vs full path - Runtime Configuration: Configure sinks, queue sizes, log level, source-location output, and timestamp formats
- Macro Fast Path: Disabled log levels skip argument evaluation before queueing
- Direct Sink Logging: Route messages to a named sink or a sink reference when a message should not be broadcast
- Shared-Library Redirection: Route plugin or strategy-library logs into a host application's logger
- Header-Only: No linking required - just include and use
- Cross-Platform: Supports Windows, Linux, and macOS
- Multi-Threaded: Safe for concurrent logging from multiple threads
- C++20: Utilizes modern C++ features
- Easy to Use: Simple macros for logging at different levels
- C++20 compatible compiler with
std::formatsupport (GCC 11+, Clang 14+, MSVC 19.29+) - CMake 3.20 or higher (for building examples/tests)
- Internet connection for downloading the slick-queue header when it is not already installed
For manual installation, you need both slick-logger and its dependency:
- Copy the
include/slick/directory to your project - Download
queue.hfrom https://raw.githubusercontent.com/SlickQuant/slick-queue/main/include/slick/queue.h - Place
queue.hin your include path or alongside the slick-logger headers
Your project structure should look like:
your_project/
├── include/
│ ├── slick/
│ └── logger.hpp
│ └── queue.h
└── src/
└── main.cpp
CMake automatically handles the slick-queue dependency for you.
cmake_minimum_required(VERSION 3.20)
project(your_project)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# Disable examples, tests, and benchmarks for slick-logger
set(BUILD_SLICK_LOGGER_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_SLICK_LOGGER_TESTING OFF CACHE BOOL "" FORCE)
set(BUILD_SLICK_LOGGER_BENCHMARKS OFF CACHE BOOL "" FORCE)
# Optional: disable LOG_* macro source-location capture at compile time
set(SLICK_LOGGER_ENABLE_SOURCE_LOCATION OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
slick-logger
GIT_REPOSITORY https://github.com/SlickQuant/slick-logger.git
GIT_TAG main
)
FetchContent_MakeAvailable(slick-logger)
add_executable(your_app main.cpp)
target_link_libraries(your_app slick::logger)If you've installed slick-logger system-wide:
cmake_minimum_required(VERSION 3.20)
project(your_project)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Optional: also accepts -DSLICK_LOGGER_ENABLE_SOURCE_LOCATION=OFF on the CMake command line
set(SLICK_LOGGER_ENABLE_SOURCE_LOCATION OFF CACHE BOOL "" FORCE)
find_package(slick-logger REQUIRED)
add_executable(your_app main.cpp)
target_link_libraries(your_app slick::logger)cmake_minimum_required(VERSION 3.20)
project(your_project)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Add slick-logger include directory
include_directories(path/to/slick-logger/include, path/to/slick-queue/include)
add_executable(your_app main.cpp)#include <slick/logger.hpp>
int main() {
// Initialize the logger (traditional way)
slick::logger::Logger::instance().init("app.log", 1024); // queue size
// Log messages - formatting happens in background thread for performance
LOG_INFO("Application started");
LOG_DEBUG("Debug value: {}", 42); // std::format style placeholders
LOG_WARN("Processed {} items", 150);
LOG_ERROR("Error in {} at line {}", "function_name", 123);
LOG_INFO("User {} balance: ${:.2f}", "Alice", 1234.56); // Format specifiers supported
// Shutdown (optional, called automatically on destruction)
slick::logger::Logger::instance().shutdown();
return 0;
}By default, each LOG_* macro entry includes the macro call-site file name and line number:
2026-07-01 12:34:56.123456 [INFO] [main.cpp:8] Application started
Use the level-specific macros for normal application logging:
LOG_TRACE("order book depth={}", depth);
LOG_DEBUG("request id={}", request_id);
LOG_INFO("connected to {}", endpoint);
LOG_WARN("retrying after {} ms", delay_ms);
LOG_ERROR("request failed: {}", reason);
LOG_FATAL("unrecoverable error: {}", reason);The macros use the singleton logger and are filtered before arguments are evaluated. If the current global level rejects a message, expensive arguments in that log call are not computed.
using namespace slick::logger;
Logger::instance().set_level(LogLevel::L_WARN);
LOG_DEBUG("expensive value: {}", build_expensive_debug_value()); // not evaluated
LOG_WARN("visible warning");
auto current_level = Logger::instance().get_level();The available levels are L_TRACE, L_DEBUG, L_INFO, L_WARN, L_ERROR, L_FATAL, and L_OFF. A log call supports up to SLICK_LOGGER_MAX_ARGS format arguments; define that macro before including slick/logger.hpp if you need a different limit.
Source-location logging is enabled by default for LOG_* macros. The default output uses only the basename, while full-path output can be enabled at runtime:
#include <slick/logger.hpp>
int main() {
using namespace slick::logger;
Logger::instance().init("app.log");
LOG_INFO("uses basename by default"); // [main.cpp:8]
Logger::instance().set_source_location_full_path_enabled(true);
LOG_INFO("uses the full SLICK_LOGGER_FILE_PATH value"); // [C:\repo\app\main.cpp:11]
Logger::instance().set_source_location_enabled(false);
LOG_INFO("source location omitted");
Logger::instance().shutdown();
}You can configure the same behavior during initialization:
using namespace slick::logger;
LogConfig config;
config.sinks.push_back(std::make_shared<FileSink>("app.log"));
config.include_source_location = true; // default
config.include_source_location_full_path = false; // default: basename only
Logger::instance().init(config);Compile-time controls must be defined before including slick/logger.hpp:
When you use CMake with FetchContent, add_subdirectory, or find_package, disable LOG_* macro source-location capture through the slick::logger target with:
cmake -S . -B build -DSLICK_LOGGER_ENABLE_SOURCE_LOCATION=OFFFor a single target, you can also set the compile definition directly:
target_compile_definitions(your_app PRIVATE SLICK_LOGGER_ENABLE_SOURCE_LOCATION=0)// Disable source-location capture in LOG_* macros entirely.
#define SLICK_LOGGER_ENABLE_SOURCE_LOCATION 0
#include <slick/logger.hpp>// Override the compile-time source path expression captured by LOG_* macros.
// Full-path runtime output can only show what this macro provides.
#define SLICK_LOGGER_FILE_PATH __FILE__
#include <slick/logger.hpp>For bridge code that receives source information from another logging layer, direct source-location overloads are also available. The const char* source path/name passed to these public overloads is copied before the entry is queued, so dynamic strings are safe:
std::string path = "C:\\repo\\app\\bridge.cpp";
slick::logger::Logger::instance().log_with_location(
slick::logger::LogLevel::L_INFO,
path.c_str(),
42,
"bridged message");
std::string basename = "bridge.cpp";
slick::logger::Logger::instance().log_with_location(
slick::logger::LogLevel::L_WARN,
path.c_str(),
basename.c_str(),
43,
"bridged message with precomputed basename");Direct sink helpers such as sink->log_info(...) route to that sink only and do not automatically attach the caller's source location. Use LOG_* macros when you want automatic call-site capture.
slick-logger uses C++20's std::format for type-aware and efficient string formatting:
#include <slick/logger.hpp>
int main() {
slick::logger::Logger::instance().init("app.log");
// Basic placeholders
LOG_INFO("Simple message: {}", "hello");
LOG_INFO("Number: {}", 42);
// Multiple arguments
LOG_INFO("User {} has {} items", "Alice", 15);
// Format specifiers (same as std::format)
LOG_INFO("Price: ${:.2f}", 29.99); // Currency with 2 decimals
LOG_INFO("Progress: {:.1f}%", 85.7); // Percentage with 1 decimal
LOG_INFO("Hex value: 0x{:x}", 255); // Hexadecimal
LOG_INFO("Binary: 0b{:b}", 42); // Binary
LOG_INFO("Scientific: {:.2e}", 12345.67); // Scientific notation
// Width and alignment
LOG_INFO("Right aligned: {:>10}", "text"); // Right align in 10 chars
LOG_INFO("Left aligned: {:<10}", "text"); // Left align in 10 chars
LOG_INFO("Centered: {:^10}", "text"); // Center in 10 chars
// Zero padding
LOG_INFO("Zero padded: {:04d}", 42); // 0042
// Custom types (as long as they support std::formatter)
std::vector<int> numbers = {1, 2, 3, 4, 5};
LOG_INFO("Vector size: {}", numbers.size());
slick::logger::Logger::instance().shutdown();
return 0;
}Benefits of std::format:
- Type-Aware Formatting: Standard C++ formatting for strings, numbers, pointers, chrono values, and custom formatter-enabled types
- Performance: Highly optimized formatting implementation
- Rich Formatting: Support for width, precision, alignment, and custom formatters
- Extensible: Easy to add custom formatters for user-defined types
- Standard: Part of C++20 standard library, no external dependencies
You can pass a pre-built std::format_args object as the single argument to any log call. This lets you capture format arguments once and reuse them, or forward a pre-built arg pack from another function:
int count = 42;
double price = 9.99;
std::string_view name = "widget";
// Capture args once, pass to logger
auto args = std::make_format_args(count, price, name);
LOG_INFO("count={} price={:.2f} name={}", args);
// Also works inline
LOG_DEBUG("x={} y={}", std::make_format_args(x, y));Note:
std::make_format_argsrequires all arguments to be lvalues. Pass temporary values via a named variable.
Limitation: Custom formatter types (types requiring a
std::formatterspecialization, represented ashandleinsidestd::format_args) are not supported. They will be logged as<handle>. Use the normal variadic log call for custom-formatted types.
#include <slick/logger.hpp>
int main() {
using namespace slick::logger;
// Setup multiple sinks
Logger::instance().clear_sinks();
Logger::instance().add_console_sink(true, true); // colors + stderr for errors
Logger::instance().add_file_sink("app.log"); // basic file logging
// Configure rotation
RotationConfig rotation;
rotation.max_file_size = 10 * 1024 * 1024; // 10MB
rotation.max_files = 5; // keep last 5 files
Logger::instance().add_rotating_file_sink("debug.log", rotation);
// Initialize with queue size
Logger::instance().init(8192);
// Logs appear in console (colored) AND both files!
LOG_INFO("Multi-sink logging is active!");
LOG_ERROR("Errors go to stderr and files");
Logger::instance().shutdown();
return 0;
}You can log messages to specific sinks by name or get a reference to a sink. Each sink also supports its own minimum log level filtering:
#include <slick/logger.hpp>
int main() {
using namespace slick::logger;
Logger::instance().clear_sinks();
Logger::instance().add_file_sink("app.log", "app_sink");
Logger::instance().add_file_sink("debug.log", "debug_sink");
Logger::instance().add_console_sink(true, false, "console");
// Set per-sink log levels (second level of filtering)
auto debug_sink = Logger::instance().get_sink("debug_sink");
if (debug_sink) {
debug_sink->set_min_level(LogLevel::L_DEBUG); // Only DEBUG and above
}
Logger::instance().init(8192);
// Log to all sinks (default behavior) - filtered by global level
LOG_INFO("This goes to all sinks that accept INFO level");
// Log to specific sink by reference
auto app_sink = Logger::instance().get_sink("app_sink");
if (app_sink) {
app_sink->log_info("This goes only to app.log");
app_sink->log_error("Error in app.log only");
}
// Direct logging to debug sink - also filtered by sink's min level
if (debug_sink) {
debug_sink->log_debug("Debug info only in debug.log");
debug_sink->log_trace("This won't appear - below sink's min level");
debug_sink->log_warn("Warning only in debug.log");
}
// You can also look up the first sink of a concrete type
auto first_file_sink = Logger::instance().get_sink<FileSink>();
if (first_file_sink) {
first_file_sink->log_info("Message routed to the first FileSink");
}
Logger::instance().shutdown();
return 0;
}Sink-Level Log Filtering:
- Each sink has its own
min_levelsetting independent of the global logger level - Messages are filtered twice: first by global logger level, then by sink-specific level
- Use
sink->set_min_level(LogLevel::L_WARN)to control what each sink accepts - This allows different sinks to have different verbosity levels
Dedicated sinks only receive messages logged directly to them, not broadcast messages from LOG_* macros:
#include <slick/logger.hpp>
int main() {
using namespace slick::logger;
Logger::instance().clear_sinks();
// Create a regular sink (receives all messages)
Logger::instance().add_file_sink("regular.log", "regular");
// Create a dedicated sink (only receives direct messages)
auto dedicated_sink = std::make_shared<FileSink>(
"dedicated.log",
TimestampFormatter::Format::WITH_MICROSECONDS,
"dedicated");
dedicated_sink->set_dedicated(true); // Mark as dedicated
Logger::instance().add_sink(dedicated_sink);
Logger::instance().init(8192);
// This goes to regular.log only (dedicated sink ignores broadcasts)
LOG_INFO("Broadcast message - regular sink only");
// This goes to dedicated.log only
dedicated_sink->log_info("Direct message to dedicated sink");
// You can also make any sink dedicated
auto regular_sink = Logger::instance().get_sink("regular");
if (regular_sink) {
regular_sink->set_dedicated(true); // Now it's dedicated too
regular_sink->log_warn("This goes to regular.log only");
}
Logger::instance().shutdown();
return 0;
}Use Cases for Dedicated Sinks:
- Audit Logging: Critical security events that should only go to specific files
- Performance Monitoring: Metrics that shouldn't clutter main application logs
- Error Isolation: Separate error streams for different components
- Compliance: Regulatory requirements for certain log types
#include <slick/logger.hpp>
int main() {
using namespace slick::logger;
// Create configuration
LogConfig config;
config.sinks.push_back(std::make_shared<ConsoleSink>(true, true));
config.sinks.push_back(std::make_shared<FileSink>("application.log"));
// Add rotating file sink for errors
RotationConfig rotation;
rotation.max_file_size = 5 * 1024 * 1024; // 5MB
rotation.max_files = 10;
config.sinks.push_back(std::make_shared<RotatingFileSink>("errors.log", rotation));
// Add daily log files
config.sinks.push_back(std::make_shared<DailyFileSink>("daily.log", RotationConfig{}));
config.min_level = LogLevel::L_INFO;
config.log_queue_size = 16384;
config.string_buffer_size = 4 * 1024 * 1024;
config.include_source_location = true;
config.include_source_location_full_path = false;
Logger::instance().init(config);
// Logs go to: console + application.log + errors.log + daily_YYYY-MM-DD.log
LOG_INFO("Advanced multi-sink setup complete!");
Logger::instance().shutdown();
return 0;
}LogConfig fields:
sinks: console, file, rotating file, daily file, or custom sinksmin_level: global minimum level, defaultLogLevel::L_TRACElog_queue_size: internal log-entry queue size, rounded up to a power of twostring_buffer_size: internal string-storage queue size, rounded up to a power of twoinclude_source_location: include file and line forLOG_*macro calls, defaulttrueinclude_source_location_full_path: use the full captured path instead of the basename, defaultfalse
using namespace slick::logger;
Logger::instance().init("app.log", 65536, 4 * 1024 * 1024);
Logger::instance().set_level(LogLevel::L_DEBUG);
Logger::instance().set_source_location_enabled(true);
Logger::instance().set_source_location_full_path_enabled(false);
LOG_INFO("queued asynchronously");
Logger::instance().flush(); // wait until entries queued so far are written
Logger::instance().shutdown(); // flush and stop the writer threadUseful controls:
init(path, log_queue_size, string_buffer_size): create a default file sink and start logginginit(config): initialize fromLogConfiginit(queue_size, string_buffer_size): start with sinks that were already addedflush(): wait for queued entries to be written while keeping the logger runningshutdown(clear_sinks = true): flush, stop the writer thread, and optionally clear sinksreset(): return the singleton to an uninitialized state; mainly intended for testsset_level()/get_level(): update or read the global level filterclear_sinks(): remove all currently registered sinks before reconfiguration
Every built-in sink supports the default microsecond timestamp format, a predefined timestamp format enum, or a custom strftime-style format string:
using namespace slick::logger;
Logger::instance().clear_sinks();
RotationConfig rotation;
Logger::instance().add_console_sink(TimestampFormatter::Format::ISO8601);
Logger::instance().add_file_sink("milliseconds.log", TimestampFormatter::Format::WITH_MILLISECONDS);
Logger::instance().add_rotating_file_sink("custom.log", rotation, "%Y-%m-%d %H:%M:%S");
Logger::instance().init();Available predefined formats:
TimestampFormatter::Format::WITH_MICROSECONDS(default)TimestampFormatter::Format::WITH_MILLISECONDSTimestampFormatter::Format::DEFAULTTimestampFormatter::Format::ISO8601TimestampFormatter::Format::TIME_ONLYTimestampFormatter::Format::CUSTOM
Because slick-logger is header-only, each binary (EXE or .dll/.so) that includes logger.hpp gets its own Logger instance. This means LOG_* calls inside a dynamically-loaded plugin will not appear in the host application's log file by default.
Use Logger::set_instance() to redirect a plugin's LOG_* calls to the host's logger:
Host application — pass its logger to the plugin after loading:
// host/main.cpp
#include <slick/logger.hpp>
// Platform-specific shared library loading omitted for brevity (LoadLibrary on Windows, dlopen on Linux)
using StrategyInitFn = void(*)(slick::logger::Logger&);
void load_strategy(void* handle) {
auto* init_fn = reinterpret_cast<StrategyInitFn>(get_symbol(handle, "strategy_init"));
if (init_fn) {
// Redirect the plugin's LOG_* macros to this process's logger
init_fn(slick::logger::Logger::instance());
}
}Plugin / strategy shared library — expose a C-linkage init function:
// strategy/strategy.cpp
#include <slick/logger.hpp>
extern "C" void strategy_init(slick::logger::Logger& framework_logger) {
// All LOG_* calls in this shared library now route to the host's logger
slick::logger::Logger::set_instance(&framework_logger);
LOG_INFO("Strategy loaded — logger connected to framework");
}
extern "C" void strategy_shutdown() {
// Optional: restore this library's own local logger on unload
slick::logger::Logger::clear_instance_override();
}Why
extern "C"? C linkage prevents name-mangling differences between compilers, ensuringget_symbol/GetProcAddress/dlsymcan reliably locate the function.
Thread-safety:
set_instance()usesstd::atomicwith acquire-release ordering. Call it once during plugin initialization, before any logging threads in the plugin start.
Unload ordering — important:
LOG_*with string literals stores a raw pointer to the format string, which lives in the plugin's code segment. The host must callLogger::instance().flush()before callingFreeLibrary/dlclose.flush()blocks until the writer thread has consumed all queued entries, then returns with the logger still running so host logging can continue normally. Usingshutdown()instead would stop the logger entirely.
Multiple plugins: Each shared library (
.dll/.so) holds its own copy ofoverride_instance_. All plugins can independently callset_instance()with the same host logger — the host logger's lock-free queue is designed for concurrent producers.
Outputs to stdout/stderr with optional ANSI color support:
- Colors: Configurable color coding by log level
- Error Routing: WARN/ERROR/FATAL can go to stderr
- Cross-Platform: Works on Windows, Linux, macOS
Basic file logging:
- Append Mode: Writes to specified file
- Thread-Safe: Single writer thread handles all file operations
- Auto-Creation: Creates directories if they don't exist
Size-based log rotation:
- Max File Size: Configurable size limit (default: 10MB)
- File Retention: Keep last N files, auto-delete oldest
- Naming:
log.txt→log_1.txt→log_2.txtetc. - Atomic Rotation: Thread-safe file rotation
Date-based log rotation:
- Daily Files: Creates new file each day
- Date Format:
filename_YYYY-MM-DD.log - Automatic: Switches files at midnight
- Retention: Configurable cleanup of old files
slick::logger::RotationConfig config;
config.max_file_size = 50 * 1024 * 1024; // 50MB
config.max_files = 10; // keep last 10 files
config.compress_old = false; // future feature
config.rotation_hour = std::chrono::hours(0); // midnight for daily rotation- TRACE: Detailed debug information
- DEBUG: General debug information
- INFO: Informational messages
- WARN: Warning messages
- ERROR: Error messages
- FATAL: Fatal error messages
The logger uses a multi-producer, single-consumer ring buffer (slick-queue) with a multi-sink architecture:
[Thread 1] ──┐
[Thread 2] ──┼──► [Lock-Free Queue] ──► [Writer Thread] ──┬──► ConsoleSink
[Thread N] ──┘ ├──► FileSink
├──► RotatingFileSink
└──► DailyFileSink
- Single Writer Thread: One dedicated thread handles all sink operations
- Lock-Free Logging: Caller threads never block on I/O operations
- Flexible Sinks: Easy to add custom sink implementations
- Atomic Operations: Thread-safe queue and sink management
For optimal performance, the logger defers string formatting to the background thread:
- Caller Thread: Captures the format pointer, source location, and owned copies of any dynamic string data
- Lock-Free Queue: Stores a compact
LogEntryin the ring buffer with minimal caller-side work - Writer Thread: Formats the message and writes it to all matching sinks
This approach moves potentially expensive formatting and I/O operations off the critical path, making logging calls extremely fast and suitable for high-frequency logging scenarios.
- Simultaneous Output: Log to console + multiple files at once
- Different Retention: Each sink can have its own rotation policy
- Performance: Single writer thread efficiently handles all sinks
- Flexibility: Mix and match sink types as needed
slick-queue is downloaded automatically during the build process from https://github.com/SlickQuant/slick-queue.
Being a header-only library provides several advantages:
- Zero Linking: No need to link against library files
- Easy Integration: Just include the headers in your project
- Template Optimization: Compiler can better optimize template instantiations
- No Binary Dependencies: No need to distribute or manage .lib/.a files
- Immediate Usage: Start logging with a single
#include <slick/logger.hpp>
Creating custom sinks is straightforward - just inherit from ISink:
class JsonSink : public slick::logger::ISink {
std::ofstream file_;
bool first_entry_ = true;
public:
explicit JsonSink(const std::filesystem::path& filename) : file_(filename) {
file_ << "[\n"; // Start JSON array
}
void write(const slick::logger::LogEntry& entry) override {
// Format as JSON - see examples/multi_sink_example.cpp for full implementation
const char* level_str = /* convert level to string */;
auto [message, _] = format_log_message(entry);
if (!first_entry_) file_ << ",\n";
first_entry_ = false;
file_ << " {\n"
<< " \"timestamp\": \"" << /* formatted timestamp */ << "\",\n"
<< " \"level\": \"" << level_str << "\",\n"
<< " \"message\": \"" << message << "\"\n"
<< " }";
}
void flush() override { file_.flush(); }
};
// Usage
Logger::instance().add_sink(std::make_shared<JsonSink>("app.json"));The repository includes comprehensive examples:
logger_example.exe: Basic usage with console + file outputmulti_sink_example.exe: Demonstrates all sink types, rotation, and custom sinkstimestamp_example.exe: Demonstrates predefined and custom timestamp formats
If you want to build the provided examples and tests:
mkdir build
cd build
cmake ..
cmake --build . --config Debug
# Run examples
./examples/Debug/logger_example.exe
./examples/Debug/multi_sink_example.exe
./examples/Debug/timestamp_example.exe
# Run tests
./tests/Debug/slick_logger_tests.exe
./tests/Debug/slick_logger_sink_tests.exe
./tests/Debug/slick_logger_timestamp_tests.exe
./tests/Debug/slick_logger_shared_lib_tests.exe