How to Create a Thread-Safe Custom Sink in spdlog
To create a thread-safe custom sink in spdlog, inherit from spdlog::sinks::base_sink<std::mutex> and override the protected virtual methods sink_it_() and flush_(); the base class handles all locking and formatter management automatically.
The gabime/spdlog library isolates log output logic through a sink hierarchy centered on the base_sink template. When you need to send log messages to a custom destination—whether a network socket, database, or proprietary API—deriving from this template allows you to create a thread-safe custom sink in spdlog without implementing mutex logic yourself.
Understanding the base_sink Template
The foundation of every thread-safe sink is the base_sink<Mutex> class defined in include/spdlog/sinks/base_sink.h. This template implements the public spdlog::sinks::sink interface while handling the thread-safety glue internally.
The class holds two critical members:
std::unique_ptr<formatter> formatter_– The pattern formatter shared with the loggerMutex mutex_– The synchronization primitive (template parameter)
When a log message arrives, the public log() method acquires mutex_ and calls your implemented sink_it_(). Similarly, flush() locks before calling flush_(). This design ensures that your implementation code runs under exclusive lock without explicit synchronization code.
Choosing the Mutex Type
The Mutex template parameter determines the thread-safety guarantee:
std::mutex– Provides full thread-safe operation; use this for*_mt(multi-threaded) sink aliasesspdlog::details::null_mutex– A zero-cost dummy mutex defined ininclude/spdlog/details/null_mutex.h; use this for*_st(single-threaded) sinks where you need no locking overhead
By instantiating your sink with std::mutex, you automatically satisfy spdlog's thread-safety requirements for concurrent logging.
Step-by-Step Implementation
Follow these steps to implement your own thread-safe sink:
-
Include the required headers – You need
spdlog/sinks/base_sink.hand optionallyspdlog/details/null_mutex.hfor the single-threaded variant -
Derive from
base_sink<std::mutex>– This establishes the thread-safe behavior -
Override
sink_it_()– Format thedetails::log_msgusingformatter_->format()and write to your destination -
Override
flush_()– Implement any destination-specific flush semantics -
Export type aliases – Provide
my_sink_mtandmy_sink_sttypedefs for convenience
Complete Working Example
Below is a complete implementation that writes to a file while maintaining an in-memory ring buffer of recent messages:
// my_custom_sink.h
#pragma once
#include <spdlog/sinks/base_sink.h>
#include <spdlog/details/null_mutex.h>
#include <spdlog/fmt/fmt.h>
#include <fstream>
#include <mutex>
#include <vector>
namespace spdlog {
namespace sinks {
template <class Mutex>
class my_custom_sink : public base_sink<Mutex>
{
public:
explicit my_custom_sink(const std::string& filename, std::size_t cache = 100)
: file_(filename, std::ios::app), cache_size_(cache) {}
protected:
void sink_it_(const details::log_msg& msg) override
{
// Format message using the base class formatter
memory_buf_t formatted;
base_sink<Mutex>::formatter_->format(msg, formatted);
// Write to file (executing under lock from base_sink)
file_.write(formatted.data(), formatted.size());
// Maintain ring buffer of recent lines
const std::size_t eol_len = std::strlen(details::os::default_eol);
if (lines_.size() < cache_size_)
{
lines_.emplace_back(formatted.begin(),
formatted.end() - static_cast<std::ptrdiff_t>(eol_len));
}
else
{
lines_.erase(lines_.begin());
lines_.emplace_back(formatted.begin(),
formatted.end() - static_cast<std::ptrdiff_t>(eol_len));
}
}
void flush_() override
{
file_.flush();
}
private:
std::ofstream file_;
std::size_t cache_size_;
std::vector<std::string> lines_;
};
// Type aliases for convenient instantiation
using my_custom_sink_mt = my_custom_sink<std::mutex>;
using my_custom_sink_st = my_custom_sink<details::null_mutex>;
} // namespace sinks
} // namespace spdlog
Usage:
#include "my_custom_sink.h"
#include <spdlog/spdlog.h>
int main()
{
auto sink = std::make_shared<spdlog::sinks::my_custom_sink_mt>("output.log", 50);
auto logger = std::make_shared<spdlog::logger>("custom_logger", sink);
spdlog::register_logger(logger);
logger->info("Thread-safe logging to custom destination");
}
Key Source Files in gabime/spdlog
For reference implementations and detailed interfaces, examine these files in the repository:
include/spdlog/sinks/sink.h– Defines the abstractsinkinterface with virtuallog(),flush(), and formatter methodsinclude/spdlog/sinks/base_sink.h– Contains thebase_sink<Mutex>template that implements the locking mechanisminclude/spdlog/details/null_mutex.h– Provides thenull_mutextype for zero-overhead single-threaded sinkstests/test_sink.h– Reference implementation showingtest_sink_mtandtest_sink_stused by the spdlog test suite
Summary
- Derive from
base_sink<std::mutex>to create thread-safe sinks; the base class automatically locks before calling yoursink_it_()implementation - Override
sink_it_()andflush_()to implement destination-specific output logic; these methods run under the protection of the mutex - Use
formatter_->format(msg, buf)to apply the configured pattern to the raw log message - Provide
_mtand_staliases usingstd::mutexandnull_mutexrespectively to follow spdlog naming conventions - Reference
tests/test_sink.hfor a minimal working example of the pattern
Frequently Asked Questions
What is the difference between _mt and _st suffixes in spdlog sinks?
The _mt suffix indicates a thread-safe sink using std::mutex, while _st indicates a single-threaded sink using spdlog::details::null_mutex. The null_mutex provides zero-overhead locking for scenarios where the logger is only accessed from a single thread, eliminating synchronization costs entirely.
Do I need to manually lock inside sink_it_() when using base_sink?
No. The base_sink template acquires the mutex in its public log() method before calling your protected sink_it_() implementation. Your code runs under exclusive lock automatically, so you should not acquire additional locks unless interfacing with external thread-unsafe resources that require their own synchronization.
Can I use a custom mutex type instead of std::mutex?
Yes. As long as the type satisfies the BasicLockable requirements (provides lock() and unlock() methods), you can pass it as the template argument to base_sink. This allows integration with custom synchronization primitives, recursive mutexes, or instrumented locks for debugging.
How do I access the formatted message string inside sink_it_()?
Call base_sink<Mutex>::formatter_->format(msg, buf) where buf is a memory_buf_t (typically fmt::memory_buffer). This populates the buffer with the formatted text according to the pattern set via set_pattern() or set_formatter(). Access the string content using buf.data() and buf.size() before writing to your 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 →