# 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. Get a complete guide to effective logging.

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

---

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

The **gabime/spdlog** library routes every log message through one or more *sinks*, which are destination handlers for files, consoles, or custom targets. When you need to send logs to a database, network endpoint, or proprietary device, creating a custom sink is the standard approach. This guide covers the concrete implementation steps using the actual source structure from the repository.

## Understanding the Sink Architecture

Before writing code, understand the two primary base classes that define sink behavior in the `include/spdlog/sinks/` directory.

### The Sink Interface

At the top of the hierarchy sits `spdlog::sinks::sink` defined in [`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h). This pure virtual interface declares the public API every sink must support: `log()`, `flush()`, `set_pattern()`, and `set_formatter()`. While you can inherit directly from this class, you would need to manually implement level filtering, mutex locking, and formatter management.

### The Base Sink Helper

For practical implementations, the library provides `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). This template class implements all the boilerplate logic, leaving you to focus only on the actual output mechanism. The template parameter determines thread safety:

- **`std::mutex`** – For thread-safe sinks used across multiple threads
- **`spdlog::details::null_mutex`** – For single-threaded contexts where locking overhead should be avoided

## Step-by-Step Implementation

Follow these concrete steps to create a functional custom sink.

### Choose Your Mutex Type

Select the mutex based on your concurrency requirements. Use `std::mutex` if the logger will be shared across threads, or `spdlog::details::null_mutex` for single-threaded applications where performance is critical.

### Implement the Required Methods

Derive from `base_sink<Mutex>` and implement two protected pure-virtual hooks:

- **`void sink_it_(const spdlog::details::log_msg& msg) override`** – Called for every log record. Access the inherited `formatter_` member to convert the message to a string, then write to your destination.
- **`void flush_() override`** – Called when the logger flushes. Perform any OS-level synchronization (e.g., `fflush`, `fsync`).

The `base_sink` parent handles acquire/release of the mutex, checks log levels, and manages the formatter lifecycle automatically.

### Handle Message Formatting

Inside `sink_it_()`, use the inherited `formatter_` member to format the message:

```cpp
spdlog::memory_buf_t formatted;
formatter_->format(msg, formatted);
std::string str = fmt::to_string(formatted);

```

The `formatter_` object is initialized automatically when the sink is constructed, defaulting to the global pattern unless explicitly changed.

## Complete Working Example

Below is a minimal, runnable example implementing a file-appending sink without thread synchronization (suitable for single-threaded use):

```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 inherits from `base_sink<null_mutex>` because file appending is inexpensive and the logger is confined to a single thread. The `formatter_` member is populated automatically, respecting any pattern set via `spdlog::set_pattern()`.

## Integration and Usage

### Creating a Logger with Your Sink

Instantiate your sink as a `std::shared_ptr` and pass it to a `spdlog::logger` constructor:

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

```

### Registering for Global Access

To retrieve the logger later via `spdlog::get("my_logger")`, register it with the global registry:

```cpp
spdlog::register_logger(logger);

```

You can also combine multiple sinks (custom and standard) by adding them to the logger's internal sinks vector before registration.

## Reference Implementation

For a production-ready reference showing state management, message counting, and artificial delay injection, examine the **test sink** implementation in [`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/tests/test_sink.h). This class demonstrates how to store the last 100 formatted lines in memory for verification purposes, providing a solid template for sinks that need to buffer or transform messages before final output.

## 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) rather than implementing the full `sink` interface manually.
- **Choose** between `std::mutex` (multi-threaded) and `spdlog::details::null_mutex` (single-threaded) as the template parameter.
- **Implement** `sink_it_()` to format and write messages, and `flush_()` to synchronize with the storage medium.
- **Access** the pre-configured formatter via the inherited `formatter_` member to ensure pattern consistency.
- **Register** custom loggers with `spdlog::register_logger()` for global access through the `spdlog::get()` API.

## Frequently Asked Questions

### Do I need to make my custom sink thread-safe?

Thread safety depends on the template parameter you pass to `base_sink`. Use `std::mutex` if multiple threads will log to the same sink instance, or `spdlog::details::null_mutex` if the sink is confined to a single thread. The base class handles all locking automatically based on your choice.

### How do I access the formatted string inside `sink_it_`?

Create a `spdlog::memory_buf_t` object and pass it to `formatter_->format(msg, formatted)`. Convert the buffer to a string using `fmt::to_string(formatted)`, or write the buffer data directly to your destination. The formatter is inherited from `base_sink` and initialized automatically.

### Can I use multiple custom sinks in one logger?

Yes. A `spdlog::logger` can hold multiple sinks in its internal vector. Pass a vector of sink pointers to the logger constructor, or call `logger->sinks().push_back()` to add additional destinations (including mixtures of custom and built-in sinks) after construction.

### What's 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 manually implement level filtering, mutex locking, and formatter storage. Inheriting from `base_sink<Mutex>` provides all this infrastructure, requiring you to implement only the two protected methods `sink_it_()` and `flush_()` for the actual output logic.