How to Create a Logger with Multiple Sinks in spdlog
In spdlog, you create a multi-sink logger by either passing a vector of std::shared_ptr<spdlog::sinks::sink> to the spdlog::logger constructor or by wrapping multiple child sinks in a spdlog::sinks::dist_sink, which broadcasts every log message to all attached destinations.
The spdlog library (gabime/spdlog) routes log messages through the sink abstraction, allowing a single logger instance to write to multiple destinations simultaneously. Understanding how to create a logger with multiple sinks in spdlog enables you to stream identical output to both the console and persistent log files while maintaining independent formatting and severity filters for each target.
Understanding the Multi-Sink Architecture
The Logger Class (include/spdlog/logger.h)
According to the spdlog source code, the spdlog::logger class maintains a std::vector<std::shared_ptr<spdlog::sinks::sink>> to store its attached sinks. When you invoke a logging method such as logger->info(), the logger iterates over this container and forwards the formatted message to each sink's sink_it_ implementation. This design means a logger naturally supports multiple sinks without additional wrapper code.
The Distribution Sink (include/spdlog/sinks/dist_sink.h)
For scenarios requiring dynamic sink management, spdlog provides spdlog::sinks::dist_sink. This convenience sink holds its own collection of child sinks and implements the sink_it_ method to forward every log request to all children. It preserves individual set_level and set_pattern configurations for each child, allowing runtime addition or removal via add_sink() and remove_sink().
Method 1: Direct Vector Initialization
The most straightforward approach constructs a vector of sink pointers and passes the iterator range directly to the logger constructor. This method minimizes overhead and works best when your sink configuration remains static after initialization.
#include <spdlog/logger.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <vector>
// Create concrete sinks with independent configurations
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::info);
console_sink->set_pattern("[%H:%M:%S] [%^%l%$] %v");
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log", true);
file_sink->set_level(spdlog::level::debug);
file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v");
// Construct logger with begin/end iterators
std::vector<spdlog::sink_ptr> sinks{console_sink, file_sink};
auto logger = std::make_shared<spdlog::logger>("multi_sink", sinks.begin(), sinks.end());
logger->set_level(spdlog::level::debug);
spdlog::register_logger(logger);
Method 2: Using dist_sink for Runtime Flexibility
Use spdlog::sinks::dist_sink when you need to modify the sink collection after logger creation. This pattern delegates sink management to the distribution sink while presenting a single interface to the logger.
#include <spdlog/sinks/dist_sink.h>
// Initialize sinks as before
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);
// Aggregate in 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);
// Logger receives the single dist_sink
auto logger = std::make_shared<spdlog::logger>("distributed", dist_sink);
logger->set_level(spdlog::level::debug);
// Runtime modification: remove file sink without reconstructing logger
// dist_sink->remove_sink(file_sink);
Configuring Independent Sink Behavior
Each concrete sink derives from spdlog::sinks::sink (defined in include/spdlog/sinks/sink.h) and implements the pure virtual methods sink_it_ and flush_. This inheritance allows every sink in a multi-sink configuration to maintain its own:
- Log level filter: Controlled via
set_level(spdlog::level::level_enum) - Output pattern: Controlled via
set_pattern(const std::string&) - Flush behavior: Triggered via
flush()or automatic flush intervals
When a log message reaches a sink with a severity below its configured threshold, that sink silently discards the message while other sinks in the chain continue processing.
Complete Working Example
The following implementation demonstrates combining console and file sinks with distinct patterns, as shown in the official repository:
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/basic_file_sink.h"
#include <vector>
void multi_sink_example()
{
// Console sink with ANSI color support
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::info);
console_sink->set_pattern("[%H:%M:%S %z] [%^%l%$] %v");
// File sink with append mode enabled
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/multisink.txt", true);
file_sink->set_level(spdlog::level::debug);
// Initialize logger with sink vector
spdlog::logger logger("multi_sink", {console_sink, file_sink});
logger.set_level(spdlog::level::debug);
// Messages dispatch to both sinks
logger.info("Application started"); // Appears in console and file
logger.debug("Debug details"); // Appears only in file (console filters at info)
}
Summary
- Core mechanism:
spdlog::loggerstores sinks in astd::vector<std::shared_ptr<spdlog::sinks::sink>>defined ininclude/spdlog/logger.h, iterating through the collection on every log call. - Construction options: Pass a vector iterator range directly to the logger constructor, or aggregate sinks within
spdlog::sinks::dist_sink(frominclude/spdlog/sinks/dist_sink.h) for runtime modification. - Per-sink configuration: Each sink maintains independent log levels and formatting patterns, allowing the console to show warnings while a file captures debug messages.
- Concrete implementations: Common sinks include
stdout_color_sink_mtfor colored console output andbasic_file_sink_mtfor file persistence, both implementing thesink_it_method.
Frequently Asked Questions
How do I create a logger with multiple sinks in spdlog?
You instantiate concrete sink classes such as spdlog::sinks::stdout_color_sink_mt and spdlog::sinks::basic_file_sink_mt, add them to a std::vector<spdlog::sink_ptr>, and pass the vector's begin and end iterators to the spdlog::logger constructor. Alternatively, wrap the sinks in a spdlog::sinks::dist_sink and pass that single sink to the logger.
What is the difference between using a vector of sinks and dist_sink in spdlog?
When you pass a vector directly to spdlog::logger, the logger iterates through the sinks itself. When using dist_sink, the logger treats the distribution sink as a single target, and the dist_sink forwards messages to its internal collection. The dist_sink approach allows runtime sink addition and removal via add_sink() and remove_sink(), whereas the vector approach requires logger reconstruction to modify the sink list.
Can each sink have a different log level when using multiple sinks in spdlog?
Yes. Each sink inherits the level_ member from spdlog::sinks::sink and filters independently. Configure individual thresholds using sink->set_level() before adding the sink to the logger or dist_sink. The logger dispatches messages to all sinks, and each sink applies its own filter.
Where can I find official examples of multi-sink loggers in spdlog?
The official gabime/spdlog repository includes working implementations in example/example.cpp, which demonstrates creating loggers with both console and file outputs. Header definitions for sink classes are located in include/spdlog/sinks/stdout_color_sinks.h for console output and include/spdlog/sinks/basic_file_sink.h for file logging.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →