How to Implement a Custom Sink in spdlog for Specialized Logging Targets
To implement a custom sink in spdlog, inherit from spdlog::sinks::base_sink<Mutex> (selecting std::mutex for thread safety or spdlog::details::null_mutex for single-threaded use), override the sink_it_() and flush_() methods, and use the inherited formatter_ member to convert log messages before writing to your target.
spdlog routes every log record through one or more sinks, making it straightforward to add custom output destinations. Whether you need to write to a database, network socket, or proprietary hardware device, implementing a custom sink in the gabime/spdlog repository requires only two method overrides when leveraging the provided base classes.
Understanding the Sink Architecture
The spdlog library separates formatting from output transport using the sink abstraction. At the top level, include/spdlog/sinks/sink.h defines the abstract interface that every sink must implement, including log(), flush(), and formatter management methods. Rather than implementing this interface directly, you should inherit from include/spdlog/sinks/base_sink.h, which provides a ready-made implementation that handles mutex locking, level filtering, and formatter lifecycle management. This leaves you responsible only for the actual output logic in sink_it_() and resource flushing in flush_().
Step-by-Step Implementation Guide
1. Select the Appropriate Mutex Type
Choose your synchronization strategy based on your threading requirements:
std::mutex– Use for thread-safe sinks that will be called from multiple threads concurrently.spdlog::details::null_mutex– Use for single-threaded sinks where locking overhead is unnecessary.
The mutex type becomes the template parameter for base_sink.
2. Inherit from base_sink
Create your class declaration by inheriting from the base template:
#include <spdlog/sinks/base_sink.h>
class my_custom_sink : public spdlog::sinks::base_sink<std::mutex> {
// Implementation details
};
For single-threaded scenarios, substitute std::mutex with spdlog::details::null_mutex.
3. Implement the Required Virtual Methods
Override these two pure virtual methods from base_sink:
-
void sink_it_(const spdlog::details::log_msg& msg) override– Called for each log record. Use the inheritedformatter_member to convert the message to a string, then write to your destination. -
void flush_() override– Called when the logger is flushed. Perform any necessary OS-level flush operations (e.g.,fflush,fsync).
The base_sink implementation automatically locks the mutex before calling these methods, so your implementation remains focused solely on output logic.
4. Instantiate and Register Your Logger
Create a shared pointer to your sink, attach it to a logger, and optionally register it with the global registry:
auto sink = std::make_shared<my_custom_sink>();
auto logger = std::make_shared<spdlog::logger>("custom_logger", sink);
spdlog::register_logger(logger);
You can combine multiple sinks by passing a vector of sink pointers to the logger constructor.
Complete Working Example
The following custom sink appends log messages to a file using single-threaded mode (null_mutex) since file appending is inexpensive and we assume single-threaded context:
#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 constructed, so output respects any pattern set via spdlog::set_pattern().
Key Source Files Reference
Understanding these three files is essential when implementing a custom sink in spdlog:
-
include/spdlog/sinks/sink.h– Defines the pure virtual sink interface (log,flush,set_pattern,set_formatter) that all sinks must implement. -
include/spdlog/sinks/base_sink.h– Provides thebase_sink<Mutex>template that implements locking, level filtering, and formatter management, leaving onlysink_it_()andflush_()to override. -
tests/test_sink.h– Contains a concrete reference implementation used by the library's test suite. It stores the last 100 formatted lines in memory, counts messages, and supports artificial delays for testing asynchronous behavior.
Summary
- Inherit from
spdlog::sinks::base_sink<Mutex>rather than the rawsinkinterface to avoid boilerplate locking and formatter management. - Choose
std::mutexfor thread-safe sinks orspdlog::details::null_mutexfor single-threaded contexts. - Implement only
sink_it_()to handle output andflush_()to handle resource synchronization. - Use the inherited
formatter_->format()method insidesink_it_()to respect user-defined formatting patterns. - Reference
tests/test_sink.hin the gabime/spdlog repository for a complete example with advanced features like message counting and delayed processing.
Frequently Asked Questions
Do I need to implement thread safety manually in a custom spdlog sink?
No. When you inherit from base_sink<Mutex>, the base class handles all synchronization automatically. The mutex is locked before sink_it_() or flush_() is called. Simply select std::mutex as the template parameter for multi-threaded environments, or spdlog::details::null_mutex for single-threaded applications where locking is unnecessary overhead.
How do I format log messages within the sink_it_() method?
The base_sink class provides a protected formatter_ member of type std::unique_ptr<spdlog::formatter>. Inside sink_it_(), create a spdlog::memory_buf_t buffer, call formatter_->format(msg, formatted) to populate it, then convert to string using fmt::to_string(formatted). This ensures your sink respects any custom pattern set by the user.
Can I use multiple custom sinks with a single logger?
Yes. You can initialize a logger with multiple sinks by passing a std::vector<sink_ptr> to the spdlog::logger constructor, or add sinks dynamically after construction using the logger->sinks().push_back() method. Each sink receives every log message that passes the logger's level filter.
What is the difference between inheriting from sink versus base_sink?
spdlog::sinks::sink is the abstract base class that requires you to implement log(), flush(), set_pattern(), and set_formatter() directly, including manual mutex management. spdlog::sinks::base_sink<Mutex> provides default implementations for all these methods, handling locking and formatter storage internally, so you only need to override the two low-level hooks: sink_it_() and flush_().
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 →