How to Create a Custom Sink for spdlog in C++

To create a custom sink for spdlog, inherit from spdlog::sinks::base_sink<Mutex> and implement the sink_it_() method to process formatted log messages and flush_() to synchronize output buffers.

spdlog routes every log record through one or more sink objects that determine where output is written. While the library ships with sinks for files, consoles, and streams, many applications require sending logs to custom destinations like databases, network services, or proprietary APIs. This guide demonstrates how to create a custom sink for spdlog using the base classes defined in the gabime/spdlog repository.

Understanding the Sink Architecture

The spdlog sink architecture separates the log message from the output destination. Two primary components make customization straightforward:

  • spdlog::sinks::sink (include/spdlog/sinks/sink.h) defines the pure virtual interface that every sink must implement, including log(), flush(), set_pattern(), and set_formatter().

  • spdlog::sinks::base_sink<Mutex> (include/spdlog/sinks/base_sink.h) provides a ready-made implementation that handles thread locking, level filtering, and formatter management. By subclassing base_sink, you only need to provide two low-level hooks for actual output.

Step-by-Step Implementation

Follow these steps to implement a functional custom sink.

Step 1: Choose a Mutex Type for Thread Safety

The base_sink template accepts a mutex type that determines thread safety:

  • std::mutex – Use for thread-safe sinks accessed by multiple threads concurrently.
  • spdlog::details::null_mutex – Use for single-threaded contexts to eliminate locking overhead.

Step 2: Inherit from base_sink

Create your class inheriting from base_sink with your chosen mutex:

#include <spdlog/sinks/base_sink.h>

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

Step 3: Implement sink_it_() and flush_()

Override these two pure virtual methods defined in base_sink.h:

  • void sink_it_(const spdlog::details::log_msg& msg) – Called for every log record. Use the inherited formatter_ member to convert msg into a string, then write it to your destination.
  • void flush_() – Called when the logger is flushed. Perform any necessary OS-level synchronization (e.g., fflush, fsync).

The base_sink automatically locks the mutex, checks log levels, and calls the formatter before invoking sink_it_(), so your implementation focuses solely on output logic.

Step 4: Add Configuration Helpers (Optional)

For sinks requiring runtime configuration (timeouts, connection strings, etc.), expose public methods to set these options. The repository's test sink (tests/test_sink.h) demonstrates this pattern with methods like set_delay() and counters for testing.

Complete Custom Sink Example

The following example implements a single-threaded file appender using null_mutex. It formats messages using the sink's inherited formatter_ and writes them to an std::ofstream.

#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
    {
        // Format the message using the sink's formatter_
        spdlog::memory_buf_t formatted;
        formatter_->format(msg, formatted);
        file_ << fmt::to_string(formatted);
    }

    void flush_() override { file_.flush(); }

private:
    std::ofstream file_;
};

Integrating the Custom Sink

After defining your sink class, instantiate it and attach it to a logger:

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!");
}

To use multiple destinations, push several sinks onto the logger's internal vector using logger->sinks().push_back(...).

Summary

  • Derive from spdlog::sinks::base_sink<Mutex> to create a custom sink for spdlog, selecting std::mutex for thread safety or null_mutex for single-threaded use.
  • Implement sink_it_() to handle formatted output using the inherited formatter_ member.
  • Implement flush_() to ensure data reaches its destination.
  • Reference the abstract interface in include/spdlog/sinks/sink.h and the helper implementation in include/spdlog/sinks/base_sink.h.
  • Study tests/test_sink.h for advanced patterns like state management and delayed logging.

Frequently Asked Questions

Do I need to implement my own locking mechanism?

No. When you inherit from spdlog::sinks::base_sink<Mutex>, the base class handles all locking before calling your sink_it_() implementation. If you inherit directly from spdlog::sinks::sink instead, you must implement log() yourself and manage thread safety manually.

What is the difference between sink_it_() and the public log() method?

The public log() method is the entry point defined in the sink interface that handles level filtering and thread synchronization. sink_it_() is the private hook called by base_sink after these concerns are addressed, allowing you to focus only on outputting the already-formatted message.

Can I use a custom sink in a multi-threaded application?

Yes. Instantiate your custom sink with std::mutex as the template parameter: class my_sink : public spdlog::sinks::base_sink<std::mutex>. This ensures thread-safe access to the sink_it_() and flush_() methods.

How do I format log messages within my custom sink?

Use the inherited formatter_ member variable. Call formatter_->format(msg, buffer) where msg is the log_msg parameter and buffer is a spdlog::memory_buf_t. This respects any pattern set via set_pattern() or set_formatter() on the sink.

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 →