SPDLog Custom Sink Implementation: A Complete Guide

Implement a custom sink by inheriting from spdlog::sinks::base_sink<Mutex> and overriding sink_it_() to handle log messages and flush_() to synchronize output, letting the base class manage thread safety and formatting.

spdlog routes every log record through one or more sinks—objects that deliver formatted output to specific destinations. To send logs to a custom target such as a database, network socket, or proprietary hardware interface, you need a SPDLog custom sink implementation. This guide demonstrates the concrete implementation strategy using the actual source code from the gabime/spdlog repository.

Understanding the Sink Architecture

The spdlog library defines a clear inheritance hierarchy for output targets. At the top is the abstract interface in include/spdlog/sinks/sink.h, which declares the pure virtual methods every sink must implement: log(), flush(), set_pattern(), and set_formatter().

Most custom implementations should inherit from include/spdlog/sinks/base_sink.h instead of implementing the raw interface directly. The base_sink template class implements level filtering, mutex locking, and formatter management, leaving you to implement only the actual output logic.

Step-by-Step Implementation

1. Choose the Appropriate Mutex Type

The base_sink template requires a mutex type parameter. Select one based on your threading requirements:

  • std::mutex – Use for thread-safe sinks that may receive logs from multiple threads concurrently.
  • spdlog::details::null_mutex – Use for single-threaded contexts where synchronization overhead is unnecessary.

2. Inherit from base_sink

Create your class inheriting from spdlog::sinks::base_sink<Mutex> as defined in include/spdlog/sinks/base_sink.h:

#include <spdlog/sinks/base_sink.h>

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

3. Implement the Two Pure-Virtual Methods

You must override two protected methods that perform the actual output:

void sink_it_(const spdlog::details::log_msg& msg) override

Called for every log record. Use the inherited formatter_ member to convert the message to a string, then write to your destination:

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

void flush_() override

Called when the logger is explicitly flushed. Perform any necessary OS-level synchronization:

void flush_() override {
    // e.g., fflush, fsync, or stream.flush()
}

4. Expose Configuration Helpers (Optional)

If your sink requires runtime configuration (e.g., connection strings, batch sizes, or artificial delays), expose public methods. The repository's tests/test_sink.h demonstrates this pattern with methods like set_delay() and counters for message inspection.

5. Create a Logger Instance

Instantiate your sink and attach it to a logger. You can combine multiple sinks by passing them to the constructor or pushing them onto the logger's sinks() vector:

auto custom_sink = std::make_shared<my_custom_sink>();
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

auto logger = std::make_shared<spdlog::logger>("multi_sink", custom_sink);
logger->sinks().push_back(console_sink);  // Combine with built-in sink

6. Register for Global Access (Optional)

To retrieve the logger via spdlog::get("multi_sink") from anywhere in your application:

spdlog::register_logger(logger);

Complete Working Example

The following implementation demonstrates a file-appending sink that uses null_mutex for single-threaded scenarios, minimizing overhead while respecting pattern settings:

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

This sink automatically respects any pattern set via spdlog::set_pattern() because it uses the inherited formatter_ member cloned from the global configuration.

Key Source Files and Internal Mechanics

Understanding the underlying implementation helps debug custom sinks:

  • include/spdlog/sinks/sink.h – Defines the pure virtual interface (log, flush, set_pattern, set_formatter) that all sinks must satisfy.
  • include/spdlog/sinks/base_sink.h – Implements the template base class handling mutex locking, level filtering, and formatter calls. Your sink_it_() runs inside a lock if using std::mutex.
  • tests/test_sink.h – Reference implementation storing the last 100 lines in memory with message counting and artificial delay capabilities.

The base_sink implementation ensures that sink_it_() is never called for messages below the sink's level threshold and that the formatter is applied consistently across all sink types.

Summary

  • Inherit from spdlog::sinks::base_sink<Mutex> located in include/spdlog/sinks/base_sink.h to minimize boilerplate while maintaining thread safety.
  • Choose std::mutex for thread safety or spdlog::details::null_mutex for single-threaded performance.
  • Implement sink_it_() to format the message using formatter_->format() and write to your destination.
  • Implement flush_() to ensure data reaches the target storage.
  • Reference tests/test_sink.h for advanced patterns like state management and configuration methods.

Frequently Asked Questions

What is the difference between inheriting from sink versus base_sink?

Inheriting directly from spdlog::sinks::sink (defined in include/spdlog/sinks/sink.h) requires you to implement locking, level filtering, and formatter management yourself. Inheriting from spdlog::sinks::base_sink<Mutex> provides these features automatically, letting you focus solely on the output logic in sink_it_() and flush_().

When should I use null_mutex instead of std::mutex?

Use spdlog::details::null_mutex when your sink operates in a single-threaded context or when you handle synchronization externally, as it eliminates locking overhead. Use std::mutex when multiple threads might log to the same sink instance concurrently.

How do I access the formatted string inside sink_it_?

Call formatter_->format(msg, buffer) passing a spdlog::memory_buf_t buffer, then convert it using fmt::to_string(buffer). The formatter_ member is inherited from base_sink and automatically initialized with the active pattern.

Can I combine a custom sink with built-in sinks in the same logger?

Yes. Create multiple sink instances and pass them to the logger constructor, or push them onto the logger's sinks() vector. For example, you can combine your custom sink with spdlog::sinks::stdout_color_sink_mt to log simultaneously to console and your custom destination.

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 →