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 protected sink_it_() and flush_() methods to handle output and flushing.

The gabime/spdlog library routes every log message through one or more sinks, which are destination handlers for files, consoles, or custom targets. When you need to send logs to a database, network endpoint, or proprietary device, creating a custom sink is the standard approach. This guide covers the concrete implementation steps using the actual source structure from the repository.

Understanding the Sink Architecture

Before writing code, understand the two primary base classes that define sink behavior in the include/spdlog/sinks/ directory.

The Sink Interface

At the top of the hierarchy sits spdlog::sinks::sink defined in include/spdlog/sinks/sink.h. This pure virtual interface declares the public API every sink must support: log(), flush(), set_pattern(), and set_formatter(). While you can inherit directly from this class, you would need to manually implement level filtering, mutex locking, and formatter management.

The Base Sink Helper

For practical implementations, the library provides spdlog::sinks::base_sink<Mutex> in include/spdlog/sinks/base_sink.h. This template class implements all the boilerplate logic, leaving you to focus only on the actual output mechanism. The template parameter determines thread safety:

  • std::mutex – For thread-safe sinks used across multiple threads
  • spdlog::details::null_mutex – For single-threaded contexts where locking overhead should be avoided

Step-by-Step Implementation

Follow these concrete steps to create a functional custom sink.

Choose Your Mutex Type

Select the mutex based on your concurrency requirements. Use std::mutex if the logger will be shared across threads, or spdlog::details::null_mutex for single-threaded applications where performance is critical.

Implement the Required Methods

Derive from base_sink<Mutex> and implement two protected pure-virtual hooks:

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

The base_sink parent handles acquire/release of the mutex, checks log levels, and manages the formatter lifecycle automatically.

Handle Message Formatting

Inside sink_it_(), use the inherited formatter_ member to format the message:

spdlog::memory_buf_t formatted;
formatter_->format(msg, formatted);
std::string str = fmt::to_string(formatted);

The formatter_ object is initialized automatically when the sink is constructed, defaulting to the global pattern unless explicitly changed.

Complete Working Example

Below is a minimal, runnable example implementing a file-appending sink without thread synchronization (suitable for single-threaded use):

#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 inherits from base_sink<null_mutex> because file appending is inexpensive and the logger is confined to a single thread. The formatter_ member is populated automatically, respecting any pattern set via spdlog::set_pattern().

Integration and Usage

Creating a Logger with Your Sink

Instantiate your sink as a std::shared_ptr and pass it to a spdlog::logger constructor:

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

Registering for Global Access

To retrieve the logger later via spdlog::get("my_logger"), register it with the global registry:

spdlog::register_logger(logger);

You can also combine multiple sinks (custom and standard) by adding them to the logger's internal sinks vector before registration.

Reference Implementation

For a production-ready reference showing state management, message counting, and artificial delay injection, examine the test sink implementation in tests/test_sink.h. This class demonstrates how to store the last 100 formatted lines in memory for verification purposes, providing a solid template for sinks that need to buffer or transform messages before final output.

Summary

  • Inherit from spdlog::sinks::base_sink<Mutex> located in include/spdlog/sinks/base_sink.h rather than implementing the full sink interface manually.
  • Choose between std::mutex (multi-threaded) and spdlog::details::null_mutex (single-threaded) as the template parameter.
  • Implement sink_it_() to format and write messages, and flush_() to synchronize with the storage medium.
  • Access the pre-configured formatter via the inherited formatter_ member to ensure pattern consistency.
  • Register custom loggers with spdlog::register_logger() for global access through the spdlog::get() API.

Frequently Asked Questions

Do I need to make my custom sink thread-safe?

Thread safety depends on the template parameter you pass to base_sink. Use std::mutex if multiple threads will log to the same sink instance, or spdlog::details::null_mutex if the sink is confined to a single thread. The base class handles all locking automatically based on your choice.

How do I access the formatted string inside sink_it_?

Create a spdlog::memory_buf_t object and pass it to formatter_->format(msg, formatted). Convert the buffer to a string using fmt::to_string(formatted), or write the buffer data directly to your destination. The formatter is inherited from base_sink and initialized automatically.

Can I use multiple custom sinks in one logger?

Yes. A spdlog::logger can hold multiple sinks in its internal vector. Pass a vector of sink pointers to the logger constructor, or call logger->sinks().push_back() to add additional destinations (including mixtures of custom and built-in sinks) after construction.

What's 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 manually implement level filtering, mutex locking, and formatter storage. Inheriting from base_sink<Mutex> provides all this infrastructure, requiring you to implement only the two protected methods sink_it_() and flush_() for the actual output logic.

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 →