# How to Implement a Custom Sink in spdlog: A Complete Guide

> Learn to implement a custom sink in spdlog by inheriting from base_sink and overriding sink_it_ and flush_ for custom log targets. Get the complete guide here.

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

---

**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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h) and [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h).

### The `sink` Interface

The abstract `sink` class in [`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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:

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

```

### Step 2: Derive from `base_sink<Mutex>`

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

```

For a concrete single-threaded sink:

```cpp
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**:

```cpp
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

```cpp
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:

```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 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`](https://github.com/gabime/spdlog/blob/main/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](https://github.com/gabime/spdlog/blob/v1.x/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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h) | Pure virtual sink interface | [`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h) |
| [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) | Template base class with locking and formatter handling | [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) |
| [`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/tests/test_sink.h) | Working example with state management and test utilities | [`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h), the recommended approach is inheriting from `base_sink<Mutex>` in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/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:

```cpp
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());

```