How to Configure spdlog to Route Different Log Levels to Different Sinks

To route different log levels to different outputs in spdlog, attach multiple sinks to a single logger and set individual minimum log levels on each sink using set_level(), allowing each sink to filter messages independently based on severity.

The gabime/spdlog library implements a flexible sink-based architecture where log routing is controlled at the sink level rather than the logger level. Each sink acts as an independent output channel that can be configured to accept or reject messages based on their severity, enabling sophisticated log distribution strategies without complex custom filters.

How Sink-Level Filtering Works

spdlog routes messages through a two-stage filtering process. First, the logger checks its own level; if the message passes, it forwards the log_msg to every attached sink. Each sink then performs its own level check via the should_log() method implemented in [base_sink.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/sinks/base_sink.h).

The key components involved are:

Because filtering occurs inside each sink's sink_it_() implementation, you can attach sinks with non-overlapping level ranges to achieve strict routing separation.

Configuring Per-Sink Log Levels

Instantiate Individual Sinks

Create separate sink instances for each output destination. Common options include stdout_color_sink_mt for console output and basic_file_sink_mt for file logging.

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

auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto error_file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("errors.log");
auto trace_file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("trace.log");

Assign Minimum Log Levels

Call set_level() on each sink to establish its acceptance threshold. Messages below this level are silently discarded by that specific sink.

console_sink->set_level(spdlog::level::info);      // Accepts INFO and above
error_file_sink->set_level(spdlog::level::err);    // Accepts only ERROR and CRITICAL
trace_file_sink->set_level(spdlog::level::trace);  // Accepts everything

Assemble the Multi-Sink Logger

Pass the configured sinks to a spdlog::logger constructor using sinks_init_list or a vector. The logger will broadcast every message to all sinks, but each sink filters independently.

auto logger = std::make_shared<spdlog::logger>("multi_sink_logger",
    spdlog::sinks_init_list{console_sink, error_file_sink, trace_file_sink});
spdlog::register_logger(logger);

Using dist_sink for Sink Groups

For complex configurations, wrap multiple sinks in a spdlog::sinks::dist_sink ([dist_sink.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/sinks/dist_sink.h)). This distributor sink forwards messages to its child sinks, allowing you to treat a group of outputs as a single unit.

#include <spdlog/sinks/dist_sink.h>

auto dist_sink = std::make_shared<spdlog::sinks::dist_sink_mt>();
dist_sink->add_sink(console_sink);
dist_sink->add_sink(error_file_sink);

auto logger = std::make_shared<spdlog::logger>("distributed", dist_sink);

Complete Working Example

The following example demonstrates routing TRACE-DEBUG to a debug file, INFO-WARN to the console, and ERROR-CRITICAL to both the console and a separate error file.

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

int main() {
    // Console: INFO and above (colorized)
    auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    console->set_level(spdlog::level::info);
    
    // Error file: ERROR and CRITICAL only
    auto error_file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("errors.log", true);
    error_file->set_level(spdlog::level::err);
    
    // Debug file: TRACE and DEBUG only
    auto debug_file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("debug.log", true);
    debug_file->set_level(spdlog::level::trace);
    // Filter out INFO and above from debug file using a custom filter or null sink for higher levels
    // Note: spdlog doesn't support max_level filter directly, so we use a trick:
    // For exclusive routing, you'd need a custom sink or check message level in formatter
    
    // Create logger with all three sinks
    auto logger = std::make_shared<spdlog::logger>("router",
        spdlog::sinks_init_list{console, error_file, debug_file});
    
    // Set logger level to lowest (trace) to ensure all messages reach the sinks
    logger->set_level(spdlog::level::trace);
    spdlog::set_default_logger(logger);
    
    // Test routing
    logger->trace("Trace message -> debug.log only");
    logger->debug("Debug message -> debug.log only");
    logger->info("Info message -> console only");
    logger->error("Error message -> console and errors.log");
    logger->critical("Critical message -> console and errors.log");
    
    logger->flush();
    return 0;
}

Note: The example above shows that debug_file will receive TRACE and DEBUG, but also ERROR and CRITICAL because those levels are higher than TRACE. For truly exclusive routing (e.g., only TRACE-DEBUG to the debug file), you would need to implement a custom sink or use a filter functor, as spdlog's set_level() only establishes a minimum threshold.

Key Source Files

File Purpose
[include/spdlog/spdlog.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/spdlog.h) Logger creation and global registry
[include/spdlog/sinks/sink.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/sinks/sink.h) Abstract base class defining the sink interface
[include/spdlog/sinks/base_sink.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/sinks/base_sink.h) CRTP implementation with should_log() level checking
[include/spdlog/sinks/dist_sink.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/sinks/dist_sink.h) Distributor sink for grouping child sinks
[include/spdlog/sinks/stdout_color_sinks.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/sinks/stdout_color_sinks.h) Console output with ANSI color support
[include/spdlog/sinks/basic_file_sink.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/sinks/basic_file_sink.h) Simple file logging implementation

Summary

  • Sink-level filtering is the idiomatic spdlog approach to route different log levels to different outputs.
  • Use set_level() on individual sinks to establish minimum severity thresholds.
  • Attach multiple sinks to a single spdlog::logger via sinks_init_list or vectors.
  • The filtering logic resides in base_sink.h, where each sink independently decides whether to process a message.
  • For complex topologies, use dist_sink to manage groups of sinks as a single unit.

Frequently Asked Questions

Can I route only a specific log level to a sink (exclusive routing)?

No, not with set_level() alone, since it only filters out messages below a minimum threshold. To achieve exclusive routing (e.g., only WARNING messages), you must implement a custom sink inheriting from spdlog::sinks::base_sink and override sink_it_() to check for exact level matches before writing. Alternatively, use a custom formatter or filter predicate if using spdlog v1.4+ filter support.

What happens if I set different levels on the logger and the sinks?

The logger acts as a first-line gate. If a message is below the logger's level, it is discarded immediately and never reaches any sinks. If it passes the logger's filter, it is forwarded to all sinks, which then apply their individual level filters. Therefore, set the logger to the lowest level (trace) when using per-sink filtering to ensure all routing decisions happen at the sink level.

Does spdlog support dynamic level changes at runtime?

Yes. Both logger->set_level() and sink->set_level() are thread-safe operations that can be called at any time. Changes take effect immediately for all subsequent log calls without requiring restarts or recompilation.

How do I prevent duplicate log messages when using multiple sinks?

Duplicate messages are the expected behavior when multiple sinks accept the same log level. If you want messages to appear in only one destination based on level, ensure your sink level ranges do not overlap. For example, set one sink to info (which accepts INFO and above) and another to debug but with a custom filter that rejects INFO and higher, or use distinct loggers rather than multiple sinks on the same logger.

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 →