Skip to main content

Global settings and best practices

spdlog's globals — the registry, the default logger, the registry-wide setters — are what make the five-minute setup possible. They're also what makes a thousand-file codebase confusing if nobody ever decided the policy on purpose. This page is the one place to make that decision.

The global setters

CallAffects
spdlog::set_level(level)Runtime level of every registered logger
spdlog::set_pattern(pattern)Pattern of every registered logger's sinks (unless overridden per sink)
spdlog::flush_on(level)Flush trigger level for every registered logger
spdlog::set_default_logger(logger)What spdlog::info(...) and friends log through
spdlog::set_error_handler(fn)What runs when a sink throws during logging
spdlog::set_automatic_registration(bool)Whether factory helpers add new loggers to the registry
spdlog::apply_all(fn)Runs fn against every registered logger — for anything not covered above

A canonical init function

logging_init.cpp
void init_logging() {
std::vector<spdlog::sink_ptr> sinks;

auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console->set_level(spdlog::level::warn);
console->set_pattern("[%^%l%$] %v");
sinks.push_back(console);

auto file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"logs/app.log", 1024 * 1024 * 10, 5);
file->set_level(spdlog::level::debug);
file->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] %v");
sinks.push_back(file);

auto logger = std::make_shared<spdlog::logger>("app", sinks.begin(), sinks.end());
logger->set_level(spdlog::level::debug);
logger->flush_on(spdlog::level::err);

spdlog::set_default_logger(logger);
spdlog::set_error_handler([](const std::string& msg) {
std::fprintf(stderr, "spdlog internal error: %s\n", msg.c_str());
});
}

Call this once, early in main(), and every unqualified spdlog::info(...) call in the rest of the program routes through it without further setup.

Error handling

spdlog::set_error_handler(...) installs a handler for exceptions thrown inside spdlog's own logging path (a sink's write failing, for example) — it's how you find out logging itself broke, without that failure taking down the calling code. Left unset, spdlog's default handler prints a message to stderr and continues.

An exception escaping your error handler terminates the process

The error handler runs inside spdlog's internals; letting an exception propagate out of it is undefined behavior in that context and typically ends in std::terminate. Keep the handler simple and exception-free — log to stderr, increment a counter, nothing that can itself throw.

Library code vs application code

A library should take a logger, not create one — or at minimum disable automatic registration

Library code that calls spdlog::stdout_color_mt("mylib") registers a name into the application's registry, where it can collide with a name the application chose, and shows up in the application's apply_all calls whether or not that's wanted. Prefer accepting a std::shared_ptr<spdlog::logger> from the caller; if you must create your own, disable automatic registration first — see The registry for the mechanism.

Pitfalls checklist

A production baseline

  1. Set SPDLOG_ACTIVE_LEVEL per build type — trace/debug in debug builds, info in release.
  2. Use one multi-sink logger as the application default rather than scattering ad hoc loggers.
  3. Use a machine-parseable, timestamped pattern in file/production sinks.
  4. Set flush_on(err) plus flush_every(3s).
  5. Reach for async only after measuring that synchronous I/O is actually the bottleneck.
  6. Call spdlog::shutdown() at the very end of main(), after every other subsystem has stopped logging.

See also