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 two pure-virtual methods sink_it_() and flush_() to handle log output and flushing for your custom target.

spdlog routes every log message through one or more sinks. A sink receives a formatted log line and forwards it to a destination such as a file, console, or custom target. When you need to log to a database, network endpoint, GUI widget, or any non-standard destination, implementing a custom spdlog sink is the correct approach. This guide walks through the exact steps based on the gabime/spdlog source code.

Understanding spdlog Sink Architecture

The spdlog library provides two primary base classes for sink implementation, located in include/spdlog/sinks/sink.h and include/spdlog/sinks/base_sink.h.

The sink Interface

The abstract sink class in include/spdlog/sinks/sink.h defines the contract every sink must fulfill:

  • log(const details::log_msg& msg) – process a log message
  • flush() – force buffered output to destination
  • set_pattern(const std::string& pattern) – change output format
  • set_formatter(std::unique_ptr<formatter> fmt) – set custom formatter

Implementing this directly requires you to handle locking, level filtering, and formatter management yourself.

The base_sink Helper Class

Most custom sinks should inherit from base_sink<Mutex> instead. This template class in include/spdlog/sinks/base_sink.h provides ready-made implementations for:

  • Mutex-protected log() calls
  • Level filtering
  • Formatter storage and access via formatter_ member

Your subclass only needs to implement the two low-level hooks: sink_it_() and flush_().

Step-by-Step: Creating a Custom spdlog Sink

Step 1: Choose Your Mutex Type

Select the template parameter for thread-safety requirements:

Mutex Type Use Case
std::mutex Multi-threaded applications (default, safest)
spdlog::details::null_mutex Single-threaded contexts, maximum performance

Include the appropriate header:

#include <spdlog/sinks/base_sink.h>
#include <spdlog/details/null_mutex.h>  // for null_mutex

Step 2: Derive from base_sink<Mutex>

template<typename Mutex>
class my_custom_sink : public spdlog::sinks::base_sink<Mutex>
{
    // implementation
};

For a concrete single-threaded sink:

class my_custom_sink : public spdlog::sinks::base_sink<spdlog::details::null_mutex>
{
    // implementation
};

Step 3: Implement sink_it_() and flush_()

These two methods are the core of your custom spdlog sink implementation:

protected:
    void sink_it_(const spdlog::details::log_msg& msg) override
    {
        // Format the message using the inherited formatter_
        spdlog::memory_buf_t formatted;
        formatter_->format(msg, formatted);
        
        // Send formatted output to your custom target
        write_to_target(fmt::to_string(formatted));
    }

    void flush_() override
    {
        // Flush any buffers to your target
        target_flush();
    }

The formatter_ member is inherited from base_sink and automatically initialized. Use it to convert log_msg structures into formatted strings that respect any pattern set via set_pattern().

Step 4: Add Target-Specific Logic

Insert your custom output logic in sink_it_(). Examples include:

  • Writing to a database connection
  • Sending over a network socket
  • Posting to a message queue
  • Updating a GUI widget

Step 5: Create and Register a Logger

auto sink = std::make_shared<my_custom_sink>(/* constructor args */);
auto logger = std::make_shared<spdlog::logger>("my_logger", sink);
spdlog::register_logger(logger);  // optional: enables global lookup via spdlog::get()

Complete Custom Sink Example

Below is a working custom spdlog sink that appends to a file using null_mutex for single-threaded efficiency:

#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 spdlog sink!");
    logger->warn("Structured logging to any target");
}

Key implementation details:

  • Uses null_mutex because file appending is inexpensive and we control thread context
  • formatter_ is automatically cloned from global settings via base_sink constructor
  • flush_() delegates to std::ofstream::flush() for OS-level synchronization

Reference Implementation: The Test Sink

The spdlog repository includes a production-quality custom sink example in tests/test_sink.h. This sink demonstrates advanced patterns:

  • Storing the last N formatted messages in memory
  • Message counting for test assertions
  • Artificial delay injection for timing tests
  • Template parameterization for mutex selection

Study this file at tests/test_sink.h for patterns like state management and configuration helpers.

Key Source Files for Custom Sink Development

File Purpose Location
include/spdlog/sinks/sink.h Pure virtual sink interface include/spdlog/sinks/sink.h
include/spdlog/sinks/base_sink.h Template base class with locking and formatter handling include/spdlog/sinks/base_sink.h
tests/test_sink.h Working example with state management and test utilities tests/test_sink.h

Summary

  • Inherit from base_sink<Mutex> rather than sink to reduce boilerplate; let spdlog handle locking and formatting
  • Choose std::mutex for thread safety or null_mutex for single-threaded performance
  • Implement only sink_it_() and flush_() – format with formatter_->format() and output to your target
  • Use formatter_ inherited member to respect global pattern settings automatically
  • Reference tests/test_sink.h for production patterns including state management

Frequently Asked Questions

Do I need to implement the sink interface directly?

No. While you can implement spdlog::sinks::sink directly in include/spdlog/sinks/sink.h, the recommended approach is inheriting from base_sink<Mutex> in include/spdlog/sinks/base_sink.h. This eliminates repetitive code for locking, level filtering, and formatter storage.

How do I make my custom sink thread-safe?

Specify std::mutex as the template parameter: class my_sink : public spdlog::sinks::base_sink<std::mutex>. The base_sink template locks around every sink_it_() and flush_() call automatically.

Can I change the output format of my custom sink?

Yes. Your sink inherits set_pattern() and set_formatter() from base_sink. The formatter_ member is automatically updated when these methods are called, so sink_it_() always uses the current format without modification.

How do I combine multiple sinks in one logger?

Push multiple sinks onto the logger's internal vector:

std::vector<spdlog::sink_ptr> sinks;
sinks.push_back(std::make_shared<spdlog::sinks::stdout_color_mt>("console"));
sinks.push_back(std::make_shared<my_custom_sink>());
auto logger = std::make_shared<spdlog::logger>("multi", sinks.begin(), sinks.end());

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 →