# SPDLog Custom Sink Implementation: A Complete Guide

> Learn how to implement a custom SPDLog sink by inheriting from base_sink. Override sink_it_() and flush_() to create versatile logging solutions. Get the complete guide.

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

---

**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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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<Mutex>

Create your class inheriting from `spdlog::sinks::base_sink<Mutex>` as defined in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h):

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

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

```cpp
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`](https://github.com/gabime/spdlog/blob/main/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:

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

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

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

```

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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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.