# How to Implement a Custom Sink in spdlog for Specialized Logging Targets

> Implement a custom spdlog sink to direct logs to specialized targets. Learn to override sink_it_() and flush_() for unique logging needs.

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

---

**To implement a custom sink in spdlog, inherit from `spdlog::sinks::base_sink<Mutex>` (selecting `std::mutex` for thread safety or `spdlog::details::null_mutex` for single-threaded use), override the `sink_it_()` and `flush_()` methods, and use the inherited `formatter_` member to convert log messages before writing to your target.**

spdlog routes every log record through one or more sinks, making it straightforward to add custom output destinations. Whether you need to write to a database, network socket, or proprietary hardware device, implementing a custom sink in the gabime/spdlog repository requires only two method overrides when leveraging the provided base classes.

## Understanding the Sink Architecture

The spdlog library separates formatting from output transport using the **sink** abstraction. At the top level, [`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h) defines the abstract interface that every sink must implement, including `log()`, `flush()`, and formatter management methods. Rather than implementing this interface directly, you should inherit from [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h), which provides a ready-made implementation that handles mutex locking, level filtering, and formatter lifecycle management. This leaves you responsible only for the actual output logic in `sink_it_()` and resource flushing in `flush_()`.

## Step-by-Step Implementation Guide

### 1. Select the Appropriate Mutex Type

Choose your synchronization strategy based on your threading requirements:

- **`std::mutex`** – Use for thread-safe sinks that will be called from multiple threads concurrently.
- **`spdlog::details::null_mutex`** – Use for single-threaded sinks where locking overhead is unnecessary.

The mutex type becomes the template parameter for `base_sink`.

### 2. Inherit from base_sink<Mutex>

Create your class declaration by inheriting from the base template:

```cpp
#include <spdlog/sinks/base_sink.h>

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

```

For single-threaded scenarios, substitute `std::mutex` with `spdlog::details::null_mutex`.

### 3. Implement the Required Virtual Methods

Override these two pure virtual methods from `base_sink`:

- **`void sink_it_(const spdlog::details::log_msg& msg) override`** – Called for each log record. Use the inherited `formatter_` member to convert the message to a string, then write to your destination.

- **`void flush_() override`** – Called when the logger is flushed. Perform any necessary OS-level flush operations (e.g., `fflush`, `fsync`).

The `base_sink` implementation automatically locks the mutex before calling these methods, so your implementation remains focused solely on output logic.

### 4. Instantiate and Register Your Logger

Create a shared pointer to your sink, attach it to a logger, and optionally register it with the global registry:

```cpp
auto sink = std::make_shared<my_custom_sink>();
auto logger = std::make_shared<spdlog::logger>("custom_logger", sink);
spdlog::register_logger(logger);

```

You can combine multiple sinks by passing a vector of sink pointers to the logger constructor.

## Complete Working Example

The following custom sink appends log messages to a file using single-threaded mode (null_mutex) since file appending is inexpensive and we assume single-threaded context:

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

```

The `formatter_` member is automatically cloned from the global pattern when the sink is constructed, so output respects any pattern set via `spdlog::set_pattern()`.

## Key Source Files Reference

Understanding these three files is essential when implementing a custom sink in spdlog:

- **[`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h)** – Defines the pure virtual sink interface (`log`, `flush`, `set_pattern`, `set_formatter`) that all sinks must implement.

- **[`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h)** – Provides the `base_sink<Mutex>` template that implements locking, level filtering, and formatter management, leaving only `sink_it_()` and `flush_()` to override.

- **[`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/tests/test_sink.h)** – Contains a concrete reference implementation used by the library's test suite. It stores the last 100 formatted lines in memory, counts messages, and supports artificial delays for testing asynchronous behavior.

## Summary

- Inherit from `spdlog::sinks::base_sink<Mutex>` rather than the raw `sink` interface to avoid boilerplate locking and formatter management.
- Choose `std::mutex` for thread-safe sinks or `spdlog::details::null_mutex` for single-threaded contexts.
- Implement only `sink_it_()` to handle output and `flush_()` to handle resource synchronization.
- Use the inherited `formatter_->format()` method inside `sink_it_()` to respect user-defined formatting patterns.
- Reference [`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/tests/test_sink.h) in the gabime/spdlog repository for a complete example with advanced features like message counting and delayed processing.

## Frequently Asked Questions

### Do I need to implement thread safety manually in a custom spdlog sink?

No. When you inherit from `base_sink<Mutex>`, the base class handles all synchronization automatically. The mutex is locked before `sink_it_()` or `flush_()` is called. Simply select `std::mutex` as the template parameter for multi-threaded environments, or `spdlog::details::null_mutex` for single-threaded applications where locking is unnecessary overhead.

### How do I format log messages within the sink_it_() method?

The `base_sink` class provides a protected `formatter_` member of type `std::unique_ptr<spdlog::formatter>`. Inside `sink_it_()`, create a `spdlog::memory_buf_t` buffer, call `formatter_->format(msg, formatted)` to populate it, then convert to string using `fmt::to_string(formatted)`. This ensures your sink respects any custom pattern set by the user.

### Can I use multiple custom sinks with a single logger?

Yes. You can initialize a logger with multiple sinks by passing a `std::vector<sink_ptr>` to the `spdlog::logger` constructor, or add sinks dynamically after construction using the `logger->sinks().push_back()` method. Each sink receives every log message that passes the logger's level filter.

### What is the difference between inheriting from sink versus base_sink?

`spdlog::sinks::sink` is the abstract base class that requires you to implement `log()`, `flush()`, `set_pattern()`, and `set_formatter()` directly, including manual mutex management. `spdlog::sinks::base_sink<Mutex>` provides default implementations for all these methods, handling locking and formatter storage internally, so you only need to override the two low-level hooks: `sink_it_()` and `flush_()`.