Creating an spdlog Logger with Multiple Sinks: A Complete Guide

You create an spdlog logger with multiple sinks by instantiating concrete destination objects (such as stdout_color_sink and basic_file_sink), aggregating them using either a dist_sink wrapper or a std::vector, and passing the collection to the spdlog::logger constructor so that every log call is broadcast to every attached destination.

Creating an spdlog logger with multiple sinks allows you to route identical log messages to several outputs—such as the console and a log file—simultaneously. The gabime/spdlog library implements this through a flexible sink architecture where the spdlog::logger class maintains a collection of sink objects and dispatches each formatted message to every sink in the collection.

How Multi-Sink Logging Works in spdlog

The core mechanism lives in include/spdlog/logger.h. The spdlog::logger class stores sinks in a std::vector<std::shared_ptr<spdlog::sinks::sink>>. When you invoke a logging method like logger->info(), the logger iterates over this internal vector and calls sink->log() on each entry, forwarding the formatted message.

Each concrete sink (located in include/spdlog/sinks/) derives from the abstract base class spdlog::sinks::sink and implements sink_it_() and flush_(). Because sinks are independent, they can each enforce their own log levels and output patterns even when attached to the same logger.

Method 1: Using the Distribution Sink (dist_sink)

The spdlog::sinks::dist_sink (defined in include/spdlog/sinks/dist_sink.h) provides a convenient wrapper that itself acts as a sink while owning a collection of child sinks. It forwards every log request to all its children, preserving their individual formatting and level settings.

This approach simplifies logger construction and allows you to add or remove sinks at runtime after the logger is created.

#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/dist_sink.h>
#include <spdlog/logger.h>

// Create individual sinks
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::info);
console_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v");

auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log", true);
file_sink->set_level(spdlog::level::debug);
file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v");

// Wrap them in a distribution sink
auto dist_sink = std::make_shared<spdlog::sinks::dist_sink_mt>();
dist_sink->add_sink(console_sink);
dist_sink->add_sink(file_sink);

// Create the logger
auto logger = std::make_shared<spdlog::logger>("multi_sink", dist_sink);
logger->set_level(spdlog::level::debug);
spdlog::register_logger(logger);

Method 2: Passing a Sink Vector Directly

Alternatively, you can bypass dist_sink and pass an iterator range directly to the spdlog::logger constructor. This method stores the sinks directly in the logger's internal vector without an intermediary wrapper.

#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/logger.h>

auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log", true);

// Collect sinks in a vector
std::vector<spdlog::sink_ptr> sinks{console_sink, file_sink};

// Pass begin/end iterators to constructor
auto logger = std::make_shared<spdlog::logger>("multi_sink", sinks.begin(), sinks.end());
logger->set_level(spdlog::level::debug);
spdlog::register_logger(logger);

Configuring Independent Sink Behavior

One advantage of multiple sinks is per-sink log levels and formatting. In the examples above, the console sink shows only info and above with colorized output, while the file sink captures debug and above with a plain text format. This filtering happens inside each sink's log() method before the message is written.

When selecting sink types, choose the _mt (multi-threaded) variants for thread-safe logging across multiple threads, or _st (single-threaded) for better performance in single-threaded contexts.

Complete Working Example

The following example demonstrates a production-ready setup combining stdout_color_sink_mt and basic_file_sink_mt via dist_sink, as shown in the official example/example.cpp:

#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/dist_sink.h>

int main() {
    // 1. Create concrete sinks
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    console_sink->set_level(spdlog::level::info);
    console_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v");

    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log", true);
    file_sink->set_level(spdlog::level::debug);
    file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v");

    // 2. Combine using dist_sink
    auto dist_sink = std::make_shared<spdlog::sinks::dist_sink_mt>();
    dist_sink->add_sink(console_sink);
    dist_sink->add_sink(file_sink);

    // 3. Build and register logger
    auto logger = std::make_shared<spdlog::logger>("multi_sink", dist_sink);
    logger->set_level(spdlog::level::debug);
    spdlog::register_logger(logger);

    // 4. Log messages
    logger->info("Application started");        // Appears in console and file
    logger->debug("Debug details here");        // Appears only in file
    logger->error("Critical system error!");    // Appears in both
    
    return 0;
}

Summary

  • The spdlog::logger class in include/spdlog/logger.h stores a std::vector of sink pointers and dispatches each log message to every sink in the collection.
  • Use spdlog::sinks::dist_sink (include/spdlog/sinks/dist_sink.h) to aggregate multiple sinks into a single object that can be modified at runtime.
  • Pass sinks directly to the logger constructor using iterator ranges (begin(), end()) to avoid the dist_sink overhead.
  • Each sink maintains its own log level and pattern, allowing fine-grained control over what appears in each destination.
  • Concrete sink implementations like stdout_color_sink_mt and basic_file_sink_mt live in include/spdlog/sinks/.

Frequently Asked Questions

What is the difference between using dist_sink and passing a vector of sinks directly?

dist_sink is itself a sink that forwards messages to an internal collection of child sinks. It allows you to add or remove sinks at runtime using add_sink() and remove_sink(). Passing a vector directly to the spdlog::logger constructor stores the sinks in the logger's internal vector without an intermediary, which is slightly more efficient but requires reconstructing the logger to change the sink set.

Can each sink have different log levels when attached to the same logger?

Yes. Each sink has its own set_level() method that filters messages independently. For example, you can configure a console sink to show only warn and above while a file sink records debug and above. The logger's global level acts as a gatekeeper, but individual sinks can apply stricter filters.

How do I add or remove sinks after the logger is created?

If you used dist_sink, call dist_sink->add_sink(new_sink) or dist_sink->remove_sink(existing_sink) at any time. If you passed a vector directly to the logger constructor, you must access the logger's internal sink vector (via logger->sinks()) and modify it, or recreate the logger with a new sink collection.

Are spdlog sinks thread-safe?

Thread safety depends on the suffix. Sinks ending with _mt (multi-threaded) use mutexes to protect concurrent access and are safe to share across threads. Sinks ending with _st (single-threaded) offer better performance but require external synchronization if used from multiple threads. Always use _mt variants for multi-sink loggers in concurrent applications.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →