# How to Implement Custom Log Sinks in spdlog: A Step-by-Step Guide

> Learn how to implement custom log sinks in spdlog. This guide details inheriting from base_sink, overriding sink_it_ and flush_ methods, and attaching your custom sink to a logger.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: how-to-guide
- Published: 2026-07-16

---

**Implementing custom log sinks in spdlog requires inheriting from `spdlog::sinks::base_sink<Mutex>`, overriding the protected `sink_it_()` method to handle formatted output, overriding `flush_()` to force delivery, and then attaching the sink to an `spdlog::logger` instance.**

spdlog is a fast, header-only C++ logging library built around a flexible sink architecture. If the built-in destinations do not meet your needs, you can implement custom log sinks in spdlog by subclassing the library's base sink templates. This article walks through the exact inheritance patterns, method signatures, and registration steps used in the gabime/spdlog source code.

## Inherit from `base_sink<Mutex>`

The canonical way to create a new destination is to derive from `spdlog::sinks::base_sink<Mutex>`, defined in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h). This base class implements locking, level filtering, and formatter management, leaving you to provide only the low-level output hooks.

### Choose the Right Mutex Type

`base_sink` is a class template that accepts a mutex type. For concurrent access from multiple threads, use `std::mutex`. If the sink is only ever used in a single-threaded context, use `spdlog::details::null_mutex` to avoid synchronization overhead.

### Implement `sink_it_` and `flush_`

You must override two pure-virtual protected methods:

- `void sink_it_(const spdlog::details::log_msg& msg) override` — Receives each log record. Format the message with the inherited `formatter_` member, then write the result to your target.
- `void flush_() override` — Called when the logger is flushed. Perform any necessary OS-level flush here.

## Minimal Custom Sink Example

The following single-threaded sink appends formatted lines to a file. It inherits from `base_sink<spdlog::details::null_mutex>` because no thread sharing is required.

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

```

## Wire the Sink into a Logger

After instantiation, wrap the sink in a `std::shared_ptr` and pass it to an `spdlog::logger`. You can register the logger globally so it is retrievable with `spdlog::get()`.

```cpp
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 combine several destinations, push multiple sinks onto the logger's internal vector before use.

## Internal Architecture

Understanding the internal layers helps you decide when to subclass `base_sink` versus implement the lower-level interface directly.

### The [`sink.h`](https://github.com/gabime/spdlog/blob/main/sink.h) Interface

The abstract contract for every destination lives in [`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h). It declares virtual methods such as `log()`, `flush()`, `set_pattern()`, and `set_formatter()`. If you inherit directly from `spdlog::sinks::sink`, you are responsible for mutex handling, level checks, and formatter calls.

### The [`base_sink.h`](https://github.com/gabime/spdlog/blob/main/base_sink.h) Helper

[`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) provides a ready-made implementation of the [`sink.h`](https://github.com/gabime/spdlog/blob/main/sink.h) interface. It acquires the mutex, filters by level, formats the message, and delegates to your `sink_it_()` and `flush_()` overrides. This is the recommended path for custom log sinks in spdlog because it removes boilerplate.

### Reference Implementation in [`test_sink.h`](https://github.com/gabime/spdlog/blob/main/test_sink.h)

The library's own test suite includes a concrete custom sink in [`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/tests/test_sink.h). This implementation stores the last 100 formatted lines in memory, counts messages, and supports an artificial delay via a `set_delay()` helper. It demonstrates how to maintain custom state inside a sink subclass.

## Summary

- Derive from `spdlog::sinks::base_sink<Mutex>` in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) to minimize boilerplate.
- Pass `std::mutex` for thread-safe sinks or `spdlog::details::null_mutex` for single-threaded sinks.
- Override `sink_it_(const spdlog::details::log_msg&)` to format and deliver output using the inherited `formatter_`.
- Override `flush_()` to guarantee delivery when the logger is flushed.
- Attach the finished sink to an `spdlog::logger` and optionally register it with `spdlog::register_logger()`.

## Frequently Asked Questions

### Do I have to use `base_sink` when implementing custom log sinks in spdlog?

No. You can inherit directly from `spdlog::sinks::sink`, defined in [`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h), if you need full control over locking and formatting. However, `base_sink` handles these concerns automatically, so it is the preferred starting point for most custom destinations.

### How do I make a custom sink thread-safe?

Instantiate `base_sink` with `std::mutex` as the template argument. The `base_sink` implementation in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) locks this mutex around every call to your `sink_it_()` and `flush_()` overrides, ensuring serialized access across threads.

### Can I change the log pattern for a single custom sink?

Yes. Because `base_sink` inherits the formatter infrastructure from `spdlog::sinks::sink`, you can call `set_pattern()` or `set_formatter()` on your sink instance. The inherited `formatter_` member will then use that pattern inside your `sink_it_()` override.

### How do I combine multiple custom sinks in one logger?

Create a logger with one sink, then push additional sinks onto the logger's `sinks()` vector. Each sink can have its own formatter and log level, allowing a single logger to route output to multiple custom destinations simultaneously.