spdlog base_sink vs sink: Understanding the Core Difference in spdlog's Sink Hierarchy
The spdlog::sinks::sink class defines the pure virtual interface that every sink must implement, while spdlog::sinks::base_sink is a CRTP-based helper template that provides default implementations for common functionality, requiring only sink_it_() and flush_() to be defined by subclasses.
The gabime/spdlog library separates sink interfaces from implementation details through two distinct abstractions in the spdlog::sinks namespace. Understanding the difference between spdlog's base_sink and sink classes is critical when extending the library with custom logging destinations, as the choice determines how much boilerplate you must write for level filtering, formatting, and thread synchronization.
spdlog::sinks::sink: The Minimal Pure Virtual Interface
Located in include/spdlog/sinks/sink.h, the sink class serves as the abstract contract that every concrete sink must satisfy. It declares four pure virtual methods that establish the fundamental operations: log(const details::log_msg&), flush(), set_pattern(const std::string&), and set_formatter(std::unique_ptr<formatter>).
When inheriting directly from sink, you must implement all four methods yourself. This abstraction provides no built-in level filtering, formatter storage, or synchronization primitives, giving you complete control over every aspect of message handling. Use this approach only when you need to bypass the standard behaviors provided by the helper base class or when implementing sinks with unique threading models that conflict with the CRTP pattern used by base_sink.
spdlog::sinks::base_sink: The CRTP Implementation Scaffold
Defined in include/spdlog/sinks/base_sink.h, base_sink is a template class using the Curiously Recurring Template Pattern (CRTP) with the signature template<typename Mutex> class base_sink. Unlike the raw interface, base_sink provides concrete implementations for log(), flush(), set_pattern(), and set_formatter() while storing a level_ member and a formatter_ pointer internally.
The base_sink template handles routine tasks such as checking should_log(level) before processing messages and acquiring locks via the Mutex template parameter. It leaves only two protected pure virtual methods for subclasses to implement: sink_it_(const details::log_msg&) for actual message output and flush_() for buffer flushing. Nearly all built-in sinks—including stdout_color_sink, basic_file_sink, and rotating_file_sink—inherit from base_sink rather than sink directly because it eliminates repetitive boilerplate.
Key Technical Differences
The distinction between these abstractions centers on implementation burden versus flexibility:
- Interface Completeness: sink requires four pure virtual implementations; base_sink requires only
sink_it_()andflush_(). - Level Filtering: sink provides no filtering logic, while base_sink automatically discards messages below the configured level via its
log()implementation. - Formatter Management: base_sink maintains a
formatter_pointer and implementsset_pattern()/set_formatter()automatically; sink forces manual formatter storage and formatting logic. - Thread Safety: base_sink uses CRTP with a
Mutextemplate parameter to enable optional locking; sink leaves thread synchronization entirely to the implementer.
Typically, you should treat sink as the low-level contract and base_sink as the standard scaffold for concrete implementations.
Code Implementation Examples
Custom Sink Using the Interface (Inheriting from sink)
When you need full control over formatting, filtering, and synchronization, inherit directly from sink and implement all virtual methods.
#include <spdlog/sinks/sink.h>
class my_raw_sink : public spdlog::sinks::sink {
public:
void log(const spdlog::details::log_msg& msg) override {
// Manual formatting required
std::string formatted = fmt::to_string(msg.payload);
// Output logic here...
}
void flush() override {
// Manual flush logic...
}
void set_pattern(const std::string& pattern) override {
// Pattern handling...
}
void set_formatter(std::unique_ptr<spdlog::formatter> formatter) override {
// Formatter storage management...
}
};
Custom Sink Using the Helper (Inheriting from base_sink)
For standard use cases, inherit from base_sink to leverage built-in filtering and thread safety.
#include <spdlog/sinks/base_sink.h>
template<typename Mutex>
class my_custom_sink : public spdlog::sinks::base_sink<Mutex> {
protected:
void sink_it_(const spdlog::details::log_msg& msg) override {
// Formatter already initialized and stored by base
fmt::memory_buffer formatted;
this->formatter_->format(msg, formatted);
// Actual output logic
std::cout << fmt::to_string(formatted);
}
void flush_() override {
std::cout << std::flush;
}
};
Notice that log(), flush(), level checking, and locking are handled by base_sink; your implementation only defines the final output operations.
Summary
spdlog::sinks::sink(include/spdlog/sinks/sink.h) defines the pure virtual interface requiring manual implementation oflog(),flush(),set_pattern(), andset_formatter().spdlog::sinks::base_sink(include/spdlog/sinks/base_sink.h) is a CRTP template implementing the sink interface, providing automatic level filtering, formatter management, and mutex locking while only requiringsink_it_()andflush_().- Choose sink when you need complete control over threading, formatting, or filtering; choose base_sink for standard sink behavior with minimal boilerplate.
- Built-in sinks like
stdout_color_sinkandrotating_file_sinkdemonstrate base_sink as the preferred foundation for spdlog extensions.
Frequently Asked Questions
Should I inherit from spdlog::sinks::sink or spdlog::sinks::base_sink when creating a custom sink?
Inherit from base_sink unless you have specific requirements that conflict with its default behavior. The base_sink class handles level filtering, formatter storage, and thread synchronization via its template parameter, reducing your implementation to just the output logic in sink_it_(). Only inherit directly from sink if you need to bypass the default level checking or implement custom formatter management that conflicts with base_sink's internal storage.
Does spdlog::sinks::base_sink provide thread safety?
Yes, base_sink uses a CRTP design with a template Mutex parameter (typically std::mutex or spdlog::details::null_mutex). When you instantiate base_sink<std::mutex>, the log() and flush() methods automatically lock the mutex before calling your sink_it_() or flush_() implementations. If you use null_mutex, the locking overhead is eliminated for single-threaded scenarios.
What happens if I don't implement sink_it_() in my base_sink subclass?
Your code will fail to compile because base_sink declares sink_it_(const details::log_msg&) as a protected pure virtual method. This is the customization point where you define the actual output logic, and base_sink calls this method from its concrete log() implementation after performing level checks and acquiring locks.
Where can I find real-world examples of sinks built on these base classes?
Examine the built-in sinks in the gabime/spdlog repository. Files like include/spdlog/sinks/stdout_color_sinks.h, include/spdlog/sinks/basic_file_sink.h, and include/spdlog/sinks/rotating_file_sink.h all inherit from base_sink. For an example of a minimal custom sink, review include/spdlog/sinks/stdout_sink.h, which demonstrates the sink_it_() and flush_() pattern used throughout the library.
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 →