SPDLog Custom Sink Implementation: A Complete Guide
Implement a custom sink by inheriting from spdlog::sinks::base_sink<Mutex> and overriding sink_it_() to handle log messages and flush_() to synchronize output, letting the base class manage thread safety and formatting.
spdlog routes every log record through one or more sinks—objects that deliver formatted output to specific destinations. To send logs to a custom target such as a database, network socket, or proprietary hardware interface, you need a SPDLog custom sink implementation. This guide demonstrates the concrete implementation strategy using the actual source code from the gabime/spdlog repository.
Understanding the Sink Architecture
The spdlog library defines a clear inheritance hierarchy for output targets. At the top is the abstract interface in include/spdlog/sinks/sink.h, which declares the pure virtual methods every sink must implement: log(), flush(), set_pattern(), and set_formatter().
Most custom implementations should inherit from include/spdlog/sinks/base_sink.h instead of implementing the raw interface directly. The base_sink template class implements level filtering, mutex locking, and formatter management, leaving you to implement only the actual output logic.
Step-by-Step Implementation
1. Choose the Appropriate Mutex Type
The base_sink template requires a mutex type parameter. Select one based on your threading requirements:
std::mutex– Use for thread-safe sinks that may receive logs from multiple threads concurrently.spdlog::details::null_mutex– Use for single-threaded contexts where synchronization overhead is unnecessary.
2. Inherit from base_sink
Create your class inheriting from spdlog::sinks::base_sink<Mutex> as defined in include/spdlog/sinks/base_sink.h:
#include <spdlog/sinks/base_sink.h>
class my_custom_sink : public spdlog::sinks::base_sink<std::mutex> {
// Implementation details...
};
3. Implement the Two Pure-Virtual Methods
You must override two protected methods that perform the actual output:
void sink_it_(const spdlog::details::log_msg& msg) override
Called for every log record. Use the inherited formatter_ member to convert the message to a string, then write to your destination:
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 custom destination
}
void flush_() override
Called when the logger is explicitly flushed. Perform any necessary OS-level synchronization:
void flush_() override {
// e.g., fflush, fsync, or stream.flush()
}
4. Expose Configuration Helpers (Optional)
If your sink requires runtime configuration (e.g., connection strings, batch sizes, or artificial delays), expose public methods. The repository's tests/test_sink.h demonstrates this pattern with methods like set_delay() and counters for message inspection.
5. Create a Logger Instance
Instantiate your sink and attach it to a logger. You can combine multiple sinks by passing them to the constructor or pushing them onto the logger's sinks() vector:
auto custom_sink = std::make_shared<my_custom_sink>();
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto logger = std::make_shared<spdlog::logger>("multi_sink", custom_sink);
logger->sinks().push_back(console_sink); // Combine with built-in sink
6. Register for Global Access (Optional)
To retrieve the logger via spdlog::get("multi_sink") from anywhere in your application:
spdlog::register_logger(logger);
Complete Working Example
The following implementation demonstrates a file-appending sink that uses null_mutex for single-threaded scenarios, minimizing overhead while respecting pattern settings:
#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!");
}
This sink automatically respects any pattern set via spdlog::set_pattern() because it uses the inherited formatter_ member cloned from the global configuration.
Key Source Files and Internal Mechanics
Understanding the underlying implementation helps debug custom sinks:
include/spdlog/sinks/sink.h– Defines the pure virtual interface (log,flush,set_pattern,set_formatter) that all sinks must satisfy.include/spdlog/sinks/base_sink.h– Implements the template base class handling mutex locking, level filtering, and formatter calls. Yoursink_it_()runs inside a lock if usingstd::mutex.tests/test_sink.h– Reference implementation storing the last 100 lines in memory with message counting and artificial delay capabilities.
The base_sink implementation ensures that sink_it_() is never called for messages below the sink's level threshold and that the formatter is applied consistently across all sink types.
Summary
- Inherit from
spdlog::sinks::base_sink<Mutex>located ininclude/spdlog/sinks/base_sink.hto minimize boilerplate while maintaining thread safety. - Choose
std::mutexfor thread safety orspdlog::details::null_mutexfor single-threaded performance. - Implement
sink_it_()to format the message usingformatter_->format()and write to your destination. - Implement
flush_()to ensure data reaches the target storage. - Reference
tests/test_sink.hfor advanced patterns like state management and configuration methods.
Frequently Asked Questions
What is the difference between inheriting from sink versus base_sink?
Inheriting directly from spdlog::sinks::sink (defined in include/spdlog/sinks/sink.h) requires you to implement locking, level filtering, and formatter management yourself. Inheriting from spdlog::sinks::base_sink<Mutex> provides these features automatically, letting you focus solely on the output logic in sink_it_() and flush_().
When should I use null_mutex instead of std::mutex?
Use spdlog::details::null_mutex when your sink operates in a single-threaded context or when you handle synchronization externally, as it eliminates locking overhead. Use std::mutex when multiple threads might log to the same sink instance concurrently.
How do I access the formatted string inside sink_it_?
Call formatter_->format(msg, buffer) passing a spdlog::memory_buf_t buffer, then convert it using fmt::to_string(buffer). The formatter_ member is inherited from base_sink and automatically initialized with the active pattern.
Can I combine a custom sink with built-in sinks in the same logger?
Yes. Create multiple sink instances and pass them to the logger constructor, or push them onto the logger's sinks() vector. For example, you can combine your custom sink with spdlog::sinks::stdout_color_sink_mt to log simultaneously to console and your custom destination.
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 →