# 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_ methods for effective logging. Get the complete guide.

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

---

**To implement a custom sink in spdlog, inherit from `spdlog::sinks::base_sink<Mutex>` and override the `sink_it_()` and `flush_()` methods to handle formatted output and flushing operations.**

spdlog routes every log message through *sink* objects that define where output goes. To send logs to custom destinations like databases, network streams, or GUI widgets, you need to implement a custom sink in spdlog by extending the provided base classes. The library's template-based design in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) handles threading, formatting, and level filtering automatically, leaving you to focus solely on the actual output logic.

## Understanding the Sink Architecture

The spdlog sink system consists of two primary abstractions. The **pure virtual interface** defined in [`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h) specifies the public API every sink must provide: `log()`, `flush()`, `set_pattern()`, and `set_formatter()`. Most custom implementations should inherit from `spdlog::sinks::base_sink<Mutex>` instead, 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 the locking, level filtering, and formatter management, requiring only two private methods to define the destination-specific behavior.

The repository's test suite provides a reference implementation in [`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/tests/test_sink.h). This example stores the last 100 formatted lines in memory while counting messages, demonstrating how to maintain custom state within a sink.

## Step-by-Step Implementation Guide

### Step 1: Select Thread Safety Level

Choose your mutex type based on the sink's usage pattern. Use `std::mutex` for **thread-safe sinks** that will be accessed from multiple threads, or `spdlog::details::null_mutex` for **single-threaded sinks** where you want to avoid locking overhead. This mutex becomes the template parameter for `base_sink`.

### Step 2: Inherit from base_sink

Create your class inheriting from `base_sink<Mutex>`:

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

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

```

### Step 3: Implement sink_it_

Override `void sink_it_(const spdlog::details::log_msg& msg) override` to receive log records. Use the inherited `formatter_` member to convert the message structure into a formatted string:

```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 destination
}

```

### Step 4: Implement flush_

Override `void flush_() override` to perform any necessary OS-level flush operations, such as `fflush()`, `fsync()`, or stream flushing:

```cpp
void flush_() override {
    // Flush your output device
}

```

## Complete Working Example

The following example implements a file-append sink using `null_mutex` for single-threaded contexts, writing formatted log messages to a specified file path:

```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 created, ensuring your output respects any formatting rules set via `spdlog::set_pattern()`.

## Integration and Registration

After implementing your sink, instantiate it as a `std::shared_ptr` and attach it to a logger:

```cpp
auto my_sink = std::make_shared<my_custom_sink>(/* args */);
auto logger = std::make_shared<spdlog::logger>("my_logger", my_sink);
spdlog::register_logger(logger);

```

To combine multiple destinations, push several sinks onto the logger's `sinks()` vector. Registration via `spdlog::register_logger()` enables global access through `spdlog::get("my_logger")`.

## Advanced Patterns

For sinks requiring runtime configuration, expose public methods to modify internal state. The [`test_sink.h`](https://github.com/gabime/spdlog/blob/main/test_sink.h) reference implementation demonstrates this pattern with methods like `set_delay()` to introduce artificial latency for testing. When designing thread-safe sinks, ensure any configuration methods lock the same mutex used by `base_sink` to prevent race conditions during logging.

## Summary

- **Inherit from `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 leverage built-in locking and formatting.
- **Choose `std::mutex`** for thread-safe sinks or `spdlog::details::null_mutex` for single-threaded performance.
- **Implement `sink_it_()`** to receive formatted messages via `formatter_->format()` and write to your destination.
- **Implement `flush_()`** to ensure data reaches the target device.
- **Reference [`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/tests/test_sink.h)** for a complete example of stateful sink implementation.
- **Register loggers** with `spdlog::register_logger()` for global accessibility.

## Frequently Asked Questions

### What is the difference between base_sink and sink?

**`spdlog::sinks::sink`** in [`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h) defines the pure virtual interface that all sinks must implement, including `log()`, `flush()`, and formatter management. **`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) provides a concrete base class that implements this interface while handling mutex locking, level filtering, and formatter storage. You should inherit from `base_sink` unless you need to implement your own locking strategy.

### Do I need to implement mutex locking myself?

No. When you inherit from `base_sink<Mutex>`, the base class handles all locking automatically. The `sink_it_()` and `flush_()` methods are called while the mutex is locked. You only need to choose the appropriate mutex type: `std::mutex` for thread safety or `spdlog::details::null_mutex` to skip locking overhead in single-threaded scenarios.

### How do I format messages in my custom sink?

Use the inherited `formatter_` member variable. Inside `sink_it_()`, create a `spdlog::memory_buf_t` buffer and call `formatter_->format(msg, formatted)`. Convert the buffer to a string using `fmt::to_string(formatted)` for output. The formatter automatically respects any pattern set via `spdlog::set_pattern()` or `set_formatter()` on the sink.

### Can I combine custom sinks with built-in sinks?

Yes. Create a logger with multiple sinks by passing a vector of `std::shared_ptr<spdlog::sinks::sink>` to the logger constructor, or add them individually via the logger's `sinks().push_back()` method. This allows you to route logs simultaneously to your custom destination and standard outputs like `stdout` or rotating files.