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 inherited formatter_ 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 the base_sink<Mutex> template that implements locking, level filtering, and formatter management, leaving only sink_it_() and flush_() 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 raw sink interface to avoid boilerplate locking and formatter management.
  • Choose std::mutex for thread-safe sinks or spdlog::details::null_mutex for single-threaded contexts.
  • Implement only sink_it_() to handle output and flush_() to handle resource synchronization.
  • Use the inherited formatter_->format() method inside sink_it_() to respect user-defined formatting patterns.
  • Reference tests/test_sink.h in 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →