How to Create a Multi‑Sink Logger with Different Levels and Formats in spdlog
A spdlog::logger aggregates multiple sink objects, each with independent set_level() and set_pattern() configurations, enabling fine‑grained control over where and how log messages are emitted.
The spdlog C++ logging library (maintained at gabime/spdlog) supports composing loggers from arbitrary collections of sinks—independent output destinations that each maintain their own filtering and formatting rules. This architecture allows you to route warnings to a concise console output while simultaneously capturing full trace‑level detail to persistent storage.
Understanding the Multi‑Sink Architecture
The spdlog source implements multi‑sink loggers through three cooperating components:
- Sink objects – Concrete classes derived from
spdlog::sinks::sinkininclude/spdlog/sinks/ - Distribution mechanism – The
spdlog::sinks::dist_sinkclass transparently forwards records to multiple child sinks - Logger object –
spdlog::loggerdefined ininclude/spdlog/logger.haggregates sinks and applies a global level filter
The log flow follows this path:
log call → logger (global level) → for each sink:
sink (sink level) → format (sink pattern) → output
Because each sink performs its own level check and formatting, you achieve complete orthogonality between destinations. The global logger level provides an additional filtering layer that works in conjunction with—not instead of—per‑sink levels.
Source Files and Key Classes
| File | Purpose |
|---|---|
include/spdlog/sinks/stdout_color_sink.h |
Console sink with ANSI color support |
include/spdlog/sinks/basic_file_sink.h |
Simple file sink with append/truncate options |
include/spdlog/sinks/dist_sink.h |
Internal distributor for multi‑sink loggers |
include/spdlog/logger.h |
Logger class definition and sink aggregation |
Complete Working Example
The following example, adapted from example/example.cpp in the spdlog repository, demonstrates a multi‑sink logger with different levels and formats—a colored console sink filtering to warnings and above, plus a file sink capturing all messages from trace upward:
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sink.h"
#include "spdlog/sinks/basic_file_sink.h"
int main()
{
// ---- Create console sink: warnings and above, colored concise format ----
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::warn);
console_sink->set_pattern("[%^%l%$] %v");
// ---- Create file sink: all messages, detailed timestamp format ----
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
"logs/multisink.txt", true /* truncate */);
file_sink->set_level(spdlog::level::trace);
file_sink->set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v");
// ---- Construct logger with sink vector; set global level ----
spdlog::logger multi_logger("multi_sink", {console_sink, file_sink});
multi_logger.set_level(spdlog::level::debug);
// ---- Emit test messages ----
multi_logger.warn("This appears in console AND file");
multi_logger.info("This appears only in file");
multi_logger.debug("Debug detail - file only");
multi_logger.trace("Trace data - file only (below global level)");
multi_logger.flush();
return 0;
}
Breaking Down the Configuration
Per‑Sink Level Filtering
Each sink implements void set_level(level::level_enum lvl) as declared in the base sink class. In the example:
console_sink->set_level(spdlog::level::warn)— discardsinfo,debug, andtracefile_sink->set_level(spdlog::level::trace)— accepts all severity levels
The logger's global level (multi_logger.set_level(spdlog::level::debug)) adds a second filter: trace calls never reach either sink because they fail the global check, despite the file sink's permissive setting.
Per‑Sink Pattern Formatting
Each sink implements void set_pattern(const std::string& pattern):
| Pattern | Effect |
|---|---|
[%^%l%$] %v |
Colored level (%^…%$ enables ANSI color), raw message |
[%Y-%m-%d %H:%M:%S] [%l] %v |
Full timestamp, level, message |
Patterns are evaluated independently per sink, allowing completely different output styles for each destination.
Thread Safety Considerations
The *_mt suffix (e.g., stdout_color_sink_mt, basic_file_sink_mt) selects multi‑threaded sinks that use internal locking. For single‑threaded applications, the *_st variants eliminate synchronization overhead. Mixing *_mt and *_st sinks within one logger is valid—the logger itself does not impose additional locking beyond what each sink provides.
Dynamic Runtime Reconfiguration
One advantage of spdlog's multi‑sink design is the ability to modify behavior without recreating objects. Both level and pattern can be changed at any time:
// Temporarily suppress console output
console_sink->set_level(spdlog::level::err);
// Switch file to more verbose format for debugging session
file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%s:%#] [%!] [%l] %v");
// Restore previous settings
console_sink->set_level(spdlog::level::warn);
Changes take effect immediately for all subsequent log calls.
Extending to Additional Sinks
The same vector-based construction supports any spdlog sink type. Common additions include:
spdlog::sinks::rotating_file_sink_mt— size‑limited rotating filesspdlog::sinks::daily_file_sink_mt— date‑based rotationspdlog::sinks::syslog_sink— POSIX syslog integrationspdlog::sinks::udp_sink— network forwardingspdlog::sinks::null_sink_mt— discard sink for benchmarks
Simply add more std::shared_ptr<spdlog::sinks::sink> objects to the initializer list passed to the spdlog::logger constructor.
Summary
- Sink objects are independent output destinations with individual
set_level()andset_pattern()controls spdlog::loggeraggregates sinks throughdist_sinkinternal distribution; global level adds filtering layer- Constructor interface:
spdlog::logger(name, {sink1, sink2, ...})creates multi‑sink loggers directly - Thread variants: Use
*_mtfor concurrent access,*_stfor single‑threaded performance - Runtime flexibility: Levels and patterns can be modified without logger reconstruction
Frequently Asked Questions
How do I add or remove sinks from an existing logger?
Sinks cannot be added to an existing spdlog::logger after construction. Create a new logger with the desired sink collection, or maintain a std::shared_ptr<spdlog::sinks::dist_sink> directly—its add_sink() and remove_sink() methods enable dynamic membership changes, then pass the dist_sink to a logger wrapping it.
Why does my trace message not appear despite the file sink being set to trace level?
The logger's global level acts as a first‑pass filter. If logger.set_level() is higher than trace, messages are discarded before reaching any sink. Both global and per‑sink levels must permit a message for it to be emitted.
Can different sinks use different time zones for timestamps?
Yes—spdlog's pattern formatter respects the global time zone source configured via spdlog::set_pattern() or per‑sink pattern strings with %z/%Z specifiers. For zone‑specific sinks, implement custom spdlog::formatter subclasses or use spdlog::sinks::callback_sink with manual timestamp generation.
What is the performance cost of multiple sinks?
Each sink executes independently, so costs are additive. The dist_sink implementation in include/spdlog/sinks/dist_sink.h iterates the sink vector with minimal overhead. For high‑throughput scenarios, prefer *_st sinks where thread safety permits, and consider spdlog::sinks::async_sink wrapper for I/O‑bound destinations.
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 →