How to Implement a Custom Sink in spdlog: A Complete Guide

To implement a custom sink in spdlog, inherit from spdlog::sinks::base_sink<Mutex> and override the sink_it_() and flush_() methods to handle formatted output and flushing operations.

spdlog routes every log message through sink objects that define where output goes. To send logs to custom destinations like databases, network streams, or GUI widgets, you need to implement a custom sink in spdlog by extending the provided base classes. The library's template-based design in include/spdlog/sinks/base_sink.h handles threading, formatting, and level filtering automatically, leaving you to focus solely on the actual output logic.

Understanding the Sink Architecture

The spdlog sink system consists of two primary abstractions. The pure virtual interface defined in include/spdlog/sinks/sink.h specifies the public API every sink must provide: log(), flush(), set_pattern(), and set_formatter(). Most custom implementations should inherit from spdlog::sinks::base_sink<Mutex> instead, defined in include/spdlog/sinks/base_sink.h. This base class implements the locking, level filtering, and formatter management, requiring only two private methods to define the destination-specific behavior.

The repository's test suite provides a reference implementation in tests/test_sink.h. This example stores the last 100 formatted lines in memory while counting messages, demonstrating how to maintain custom state within a sink.

Step-by-Step Implementation Guide

Step 1: Select Thread Safety Level

Choose your mutex type based on the sink's usage pattern. Use std::mutex for thread-safe sinks that will be accessed from multiple threads, or spdlog::details::null_mutex for single-threaded sinks where you want to avoid locking overhead. This mutex becomes the template parameter for base_sink.

Step 2: Inherit from base_sink

Create your class inheriting from base_sink<Mutex>:

#include <spdlog/sinks/base_sink.h>

class my_custom_sink : public spdlog::sinks::base_sink<std::mutex> {
    // Implementation follows
};

Step 3: Implement sink_it_

Override void sink_it_(const spdlog::details::log_msg& msg) override to receive log records. Use the inherited formatter_ member to convert the message structure into a formatted string:

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 destination
}

Step 4: Implement flush_

Override void flush_() override to perform any necessary OS-level flush operations, such as fflush(), fsync(), or stream flushing:

void flush_() override {
    // Flush your output device
}

Complete Working Example

The following example implements a file-append sink using null_mutex for single-threaded contexts, writing formatted log messages to a specified file path:

#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 created, ensuring your output respects any formatting rules set via spdlog::set_pattern().

Integration and Registration

After implementing your sink, instantiate it as a std::shared_ptr and attach it to a logger:

auto my_sink = std::make_shared<my_custom_sink>(/* args */);
auto logger = std::make_shared<spdlog::logger>("my_logger", my_sink);
spdlog::register_logger(logger);

To combine multiple destinations, push several sinks onto the logger's sinks() vector. Registration via spdlog::register_logger() enables global access through spdlog::get("my_logger").

Advanced Patterns

For sinks requiring runtime configuration, expose public methods to modify internal state. The test_sink.h reference implementation demonstrates this pattern with methods like set_delay() to introduce artificial latency for testing. When designing thread-safe sinks, ensure any configuration methods lock the same mutex used by base_sink to prevent race conditions during logging.

Summary

  • Inherit from base_sink<Mutex> located in include/spdlog/sinks/base_sink.h to leverage built-in locking and formatting.
  • Choose std::mutex for thread-safe sinks or spdlog::details::null_mutex for single-threaded performance.
  • Implement sink_it_() to receive formatted messages via formatter_->format() and write to your destination.
  • Implement flush_() to ensure data reaches the target device.
  • Reference tests/test_sink.h for a complete example of stateful sink implementation.
  • Register loggers with spdlog::register_logger() for global accessibility.

Frequently Asked Questions

What is the difference between base_sink and sink?

spdlog::sinks::sink in include/spdlog/sinks/sink.h defines the pure virtual interface that all sinks must implement, including log(), flush(), and formatter management. spdlog::sinks::base_sink<Mutex> in include/spdlog/sinks/base_sink.h provides a concrete base class that implements this interface while handling mutex locking, level filtering, and formatter storage. You should inherit from base_sink unless you need to implement your own locking strategy.

Do I need to implement mutex locking myself?

No. When you inherit from base_sink<Mutex>, the base class handles all locking automatically. The sink_it_() and flush_() methods are called while the mutex is locked. You only need to choose the appropriate mutex type: std::mutex for thread safety or spdlog::details::null_mutex to skip locking overhead in single-threaded scenarios.

How do I format messages in my custom sink?

Use the inherited formatter_ member variable. Inside sink_it_(), create a spdlog::memory_buf_t buffer and call formatter_->format(msg, formatted). Convert the buffer to a string using fmt::to_string(formatted) for output. The formatter automatically respects any pattern set via spdlog::set_pattern() or set_formatter() on the sink.

Can I combine custom sinks with built-in sinks?

Yes. Create a logger with multiple sinks by passing a vector of std::shared_ptr<spdlog::sinks::sink> to the logger constructor, or add them individually via the logger's sinks().push_back() method. This allows you to route logs simultaneously to your custom destination and standard outputs like stdout or rotating files.

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 →