How to Implement a Custom Sink in spdlog: A Complete Guide
To implement a custom sink in spdlog, inherit from spdlog::sinks::base_sink<Mutex> and override the sink_it_() and flush_() methods to handle formatted output and flushing operations.
spdlog routes every log message through sink objects that define where output goes. To send logs to custom destinations like databases, network streams, or GUI widgets, you need to implement a custom sink in spdlog by extending the provided base classes. The library's template-based design in include/spdlog/sinks/base_sink.h handles threading, formatting, and level filtering automatically, leaving you to focus solely on the actual output logic.
Understanding the Sink Architecture
The spdlog sink system consists of two primary abstractions. The pure virtual interface defined in include/spdlog/sinks/sink.h specifies the public API every sink must provide: log(), flush(), set_pattern(), and set_formatter(). Most custom implementations should inherit from spdlog::sinks::base_sink<Mutex> instead, defined in include/spdlog/sinks/base_sink.h. This base class implements the locking, level filtering, and formatter management, requiring only two private methods to define the destination-specific behavior.
The repository's test suite provides a reference implementation in tests/test_sink.h. This example stores the last 100 formatted lines in memory while counting messages, demonstrating how to maintain custom state within a sink.
Step-by-Step Implementation Guide
Step 1: Select Thread Safety Level
Choose your mutex type based on the sink's usage pattern. Use std::mutex for thread-safe sinks that will be accessed from multiple threads, or spdlog::details::null_mutex for single-threaded sinks where you want to avoid locking overhead. This mutex becomes the template parameter for base_sink.
Step 2: Inherit from base_sink
Create your class inheriting from base_sink<Mutex>:
#include <spdlog/sinks/base_sink.h>
class my_custom_sink : public spdlog::sinks::base_sink<std::mutex> {
// Implementation follows
};
Step 3: Implement sink_it_
Override void sink_it_(const spdlog::details::log_msg& msg) override to receive log records. Use the inherited formatter_ member to convert the message structure into a formatted string:
void sink_it_(const spdlog::details::log_msg& msg) override {
spdlog::memory_buf_t formatted;
formatter_->format(msg, formatted);
// Write fmt::to_string(formatted) to your destination
}
Step 4: Implement flush_
Override void flush_() override to perform any necessary OS-level flush operations, such as fflush(), fsync(), or stream flushing:
void flush_() override {
// Flush your output device
}
Complete Working Example
The following example implements a file-append sink using null_mutex for single-threaded contexts, writing formatted log messages to a specified file path:
#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!");
}
The formatter_ member is automatically cloned from the global pattern when the sink is created, ensuring your output respects any formatting rules set via spdlog::set_pattern().
Integration and Registration
After implementing your sink, instantiate it as a std::shared_ptr and attach it to a logger:
auto my_sink = std::make_shared<my_custom_sink>(/* args */);
auto logger = std::make_shared<spdlog::logger>("my_logger", my_sink);
spdlog::register_logger(logger);
To combine multiple destinations, push several sinks onto the logger's sinks() vector. Registration via spdlog::register_logger() enables global access through spdlog::get("my_logger").
Advanced Patterns
For sinks requiring runtime configuration, expose public methods to modify internal state. The test_sink.h reference implementation demonstrates this pattern with methods like set_delay() to introduce artificial latency for testing. When designing thread-safe sinks, ensure any configuration methods lock the same mutex used by base_sink to prevent race conditions during logging.
Summary
- Inherit from
base_sink<Mutex>located ininclude/spdlog/sinks/base_sink.hto leverage built-in locking and formatting. - Choose
std::mutexfor thread-safe sinks orspdlog::details::null_mutexfor single-threaded performance. - Implement
sink_it_()to receive formatted messages viaformatter_->format()and write to your destination. - Implement
flush_()to ensure data reaches the target device. - Reference
tests/test_sink.hfor a complete example of stateful sink implementation. - Register loggers with
spdlog::register_logger()for global accessibility.
Frequently Asked Questions
What is the difference between base_sink and sink?
spdlog::sinks::sink in include/spdlog/sinks/sink.h defines the pure virtual interface that all sinks must implement, including log(), flush(), and formatter management. spdlog::sinks::base_sink<Mutex> in include/spdlog/sinks/base_sink.h provides a concrete base class that implements this interface while handling mutex locking, level filtering, and formatter storage. You should inherit from base_sink unless you need to implement your own locking strategy.
Do I need to implement mutex locking myself?
No. When you inherit from base_sink<Mutex>, the base class handles all locking automatically. The sink_it_() and flush_() methods are called while the mutex is locked. You only need to choose the appropriate mutex type: std::mutex for thread safety or spdlog::details::null_mutex to skip locking overhead in single-threaded scenarios.
How do I format messages in my custom sink?
Use the inherited formatter_ member variable. Inside sink_it_(), create a spdlog::memory_buf_t buffer and call formatter_->format(msg, formatted). Convert the buffer to a string using fmt::to_string(formatted) for output. The formatter automatically respects any pattern set via spdlog::set_pattern() or set_formatter() on the sink.
Can I combine custom sinks with built-in sinks?
Yes. Create a logger with multiple sinks by passing a vector of std::shared_ptr<spdlog::sinks::sink> to the logger constructor, or add them individually via the logger's sinks().push_back() method. This allows you to route logs simultaneously to your custom destination and standard outputs like stdout or rotating files.
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 →