How to Implement Custom Log Sinks in spdlog: A Step-by-Step Guide
Implementing custom log sinks in spdlog requires inheriting from spdlog::sinks::base_sink<Mutex>, overriding the protected sink_it_() method to handle formatted output, overriding flush_() to force delivery, and then attaching the sink to an spdlog::logger instance.
spdlog is a fast, header-only C++ logging library built around a flexible sink architecture. If the built-in destinations do not meet your needs, you can implement custom log sinks in spdlog by subclassing the library's base sink templates. This article walks through the exact inheritance patterns, method signatures, and registration steps used in the gabime/spdlog source code.
Inherit from base_sink<Mutex>
The canonical way to create a new destination is to derive from spdlog::sinks::base_sink<Mutex>, defined in include/spdlog/sinks/base_sink.h. This base class implements locking, level filtering, and formatter management, leaving you to provide only the low-level output hooks.
Choose the Right Mutex Type
base_sink is a class template that accepts a mutex type. For concurrent access from multiple threads, use std::mutex. If the sink is only ever used in a single-threaded context, use spdlog::details::null_mutex to avoid synchronization overhead.
Implement sink_it_ and flush_
You must override two pure-virtual protected methods:
void sink_it_(const spdlog::details::log_msg& msg) override— Receives each log record. Format the message with the inheritedformatter_member, then write the result to your target.void flush_() override— Called when the logger is flushed. Perform any necessary OS-level flush here.
Minimal Custom Sink Example
The following single-threaded sink appends formatted lines to a file. It inherits from base_sink<spdlog::details::null_mutex> because no thread sharing is required.
#include <spdlog/spdlog.h>
#include <spdlog/sinks/base_sink.h>
#include <spdlog/details/null_mutex.h>
#include <fstream>
class file_append_sink : public spdlog::sinks::base_sink<spdlog::details::null_mutex>
{
public:
explicit file_append_sink(const std::string& path) : file_(path, std::ios::app) {}
protected:
void sink_it_(const spdlog::details::log_msg& msg) override
{
spdlog::memory_buf_t formatted;
formatter_->format(msg, formatted);
file_ << fmt::to_string(formatted);
}
void flush_() override { file_.flush(); }
private:
std::ofstream file_;
};
int main()
{
auto sink = std::make_shared<file_append_sink>("mylog.txt");
auto logger = std::make_shared<spdlog::logger>("custom", sink);
spdlog::register_logger(logger);
logger->info("Hello from a custom sink!");
}
Wire the Sink into a Logger
After instantiation, wrap the sink in a std::shared_ptr and pass it to an spdlog::logger. You can register the logger globally so it is retrievable with spdlog::get().
auto sink = std::make_shared<file_append_sink>("mylog.txt");
auto logger = std::make_shared<spdlog::logger>("custom", sink);
spdlog::register_logger(logger);
logger->info("Hello from a custom sink!");
To combine several destinations, push multiple sinks onto the logger's internal vector before use.
Internal Architecture
Understanding the internal layers helps you decide when to subclass base_sink versus implement the lower-level interface directly.
The sink.h Interface
The abstract contract for every destination lives in include/spdlog/sinks/sink.h. It declares virtual methods such as log(), flush(), set_pattern(), and set_formatter(). If you inherit directly from spdlog::sinks::sink, you are responsible for mutex handling, level checks, and formatter calls.
The base_sink.h Helper
include/spdlog/sinks/base_sink.h provides a ready-made implementation of the sink.h interface. It acquires the mutex, filters by level, formats the message, and delegates to your sink_it_() and flush_() overrides. This is the recommended path for custom log sinks in spdlog because it removes boilerplate.
Reference Implementation in test_sink.h
The library's own test suite includes a concrete custom sink in tests/test_sink.h. This implementation stores the last 100 formatted lines in memory, counts messages, and supports an artificial delay via a set_delay() helper. It demonstrates how to maintain custom state inside a sink subclass.
Summary
- Derive from
spdlog::sinks::base_sink<Mutex>ininclude/spdlog/sinks/base_sink.hto minimize boilerplate. - Pass
std::mutexfor thread-safe sinks orspdlog::details::null_mutexfor single-threaded sinks. - Override
sink_it_(const spdlog::details::log_msg&)to format and deliver output using the inheritedformatter_. - Override
flush_()to guarantee delivery when the logger is flushed. - Attach the finished sink to an
spdlog::loggerand optionally register it withspdlog::register_logger().
Frequently Asked Questions
Do I have to use base_sink when implementing custom log sinks in spdlog?
No. You can inherit directly from spdlog::sinks::sink, defined in include/spdlog/sinks/sink.h, if you need full control over locking and formatting. However, base_sink handles these concerns automatically, so it is the preferred starting point for most custom destinations.
How do I make a custom sink thread-safe?
Instantiate base_sink with std::mutex as the template argument. The base_sink implementation in include/spdlog/sinks/base_sink.h locks this mutex around every call to your sink_it_() and flush_() overrides, ensuring serialized access across threads.
Can I change the log pattern for a single custom sink?
Yes. Because base_sink inherits the formatter infrastructure from spdlog::sinks::sink, you can call set_pattern() or set_formatter() on your sink instance. The inherited formatter_ member will then use that pattern inside your sink_it_() override.
How do I combine multiple custom sinks in one logger?
Create a logger with one sink, then push additional sinks onto the logger's sinks() vector. Each sink can have its own formatter and log level, allowing a single logger to route output to multiple custom destinations simultaneously.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →