How to Create a Custom Sink for spdlog in C++
To create a custom sink for spdlog, inherit from spdlog::sinks::base_sink<Mutex> and implement the sink_it_() method to process formatted log messages and flush_() to synchronize output buffers.
spdlog routes every log record through one or more sink objects that determine where output is written. While the library ships with sinks for files, consoles, and streams, many applications require sending logs to custom destinations like databases, network services, or proprietary APIs. This guide demonstrates how to create a custom sink for spdlog using the base classes defined in the gabime/spdlog repository.
Understanding the Sink Architecture
The spdlog sink architecture separates the log message from the output destination. Two primary components make customization straightforward:
-
spdlog::sinks::sink(include/spdlog/sinks/sink.h) defines the pure virtual interface that every sink must implement, includinglog(),flush(),set_pattern(), andset_formatter(). -
spdlog::sinks::base_sink<Mutex>(include/spdlog/sinks/base_sink.h) provides a ready-made implementation that handles thread locking, level filtering, and formatter management. By subclassingbase_sink, you only need to provide two low-level hooks for actual output.
Step-by-Step Implementation
Follow these steps to implement a functional custom sink.
Step 1: Choose a Mutex Type for Thread Safety
The base_sink template accepts a mutex type that determines thread safety:
std::mutex– Use for thread-safe sinks accessed by multiple threads concurrently.spdlog::details::null_mutex– Use for single-threaded contexts to eliminate locking overhead.
Step 2: Inherit from base_sink
Create your class inheriting from base_sink with your chosen mutex:
#include <spdlog/sinks/base_sink.h>
class my_custom_sink : public spdlog::sinks::base_sink<std::mutex> {
// Implementation details...
};
Step 3: Implement sink_it_() and flush_()
Override these two pure virtual methods defined in base_sink.h:
void sink_it_(const spdlog::details::log_msg& msg)– Called for every log record. Use the inheritedformatter_member to convertmsginto a string, then write it to your destination.void flush_()– Called when the logger is flushed. Perform any necessary OS-level synchronization (e.g.,fflush,fsync).
The base_sink automatically locks the mutex, checks log levels, and calls the formatter before invoking sink_it_(), so your implementation focuses solely on output logic.
Step 4: Add Configuration Helpers (Optional)
For sinks requiring runtime configuration (timeouts, connection strings, etc.), expose public methods to set these options. The repository's test sink (tests/test_sink.h) demonstrates this pattern with methods like set_delay() and counters for testing.
Complete Custom Sink Example
The following example implements a single-threaded file appender using null_mutex. It formats messages using the sink's inherited formatter_ and writes them to an std::ofstream.
#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
{
// Format the message using the sink's formatter_
spdlog::memory_buf_t formatted;
formatter_->format(msg, formatted);
file_ << fmt::to_string(formatted);
}
void flush_() override { file_.flush(); }
private:
std::ofstream file_;
};
Integrating the Custom Sink
After defining your sink class, instantiate it and attach it to a logger:
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!");
}
To use multiple destinations, push several sinks onto the logger's internal vector using logger->sinks().push_back(...).
Summary
- Derive from
spdlog::sinks::base_sink<Mutex>to create a custom sink for spdlog, selectingstd::mutexfor thread safety ornull_mutexfor single-threaded use. - Implement
sink_it_()to handle formatted output using the inheritedformatter_member. - Implement
flush_()to ensure data reaches its destination. - Reference the abstract interface in
include/spdlog/sinks/sink.hand the helper implementation ininclude/spdlog/sinks/base_sink.h. - Study
tests/test_sink.hfor advanced patterns like state management and delayed logging.
Frequently Asked Questions
Do I need to implement my own locking mechanism?
No. When you inherit from spdlog::sinks::base_sink<Mutex>, the base class handles all locking before calling your sink_it_() implementation. If you inherit directly from spdlog::sinks::sink instead, you must implement log() yourself and manage thread safety manually.
What is the difference between sink_it_() and the public log() method?
The public log() method is the entry point defined in the sink interface that handles level filtering and thread synchronization. sink_it_() is the private hook called by base_sink after these concerns are addressed, allowing you to focus only on outputting the already-formatted message.
Can I use a custom sink in a multi-threaded application?
Yes. Instantiate your custom sink with std::mutex as the template parameter: class my_sink : public spdlog::sinks::base_sink<std::mutex>. This ensures thread-safe access to the sink_it_() and flush_() methods.
How do I format log messages within my custom sink?
Use the inherited formatter_ member variable. Call formatter_->format(msg, buffer) where msg is the log_msg parameter and buffer is a spdlog::memory_buf_t. This respects any pattern set via set_pattern() or set_formatter() on the sink.
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 →