How spdlog's Registry Manages Loggers and the Default Logger
spdlog uses a global thread-safe registry singleton to store all named loggers as std::shared_ptr instances, while the default logger is initialized automatically at startup and accessed via raw pointers for zero-overhead performance.
The gabime/spdlog library relies on a centralized registry system to track logger lifecycles and enforce global configuration across your application. Understanding how spdlog's registry manages the default logger is essential for building high-performance C++ applications that require runtime logger replacement or thread-safe sink configurations.
The Global Registry Architecture
The registry is implemented as a singleton pattern in include/spdlog/details/registry.h and include/spdlog/details/registry-inl.h, serving as the authoritative source for all logger instances within a process.
Singleton Design and Thread Safety
Access the global registry via spdlog::details::registry::instance(). This singleton maintains an internal std::unordered_map<std::string, std::shared_ptr<logger>> loggers_ that requires mutex protection. All mutating operations—including register_logger(), drop(), and set_level()—acquire a lock on logger_map_mutex_ to ensure thread-safe access to the storage map.
Global Configuration Storage
Beyond logger storage, the registry holds global settings that propagate to each registered instance. These include the global formatter, log level, flush level, back-trace settings, and a thread-pool reference. When you create a new logger via initialize_logger(), the registry automatically applies these global settings to ensure consistent behavior across your logging hierarchy.
How spdlog's Default Logger Works
The default logger is a special instance created during registry construction, optimized for immediate use without explicit registration steps.
Automatic Initialization at Startup
During the registry constructor in registry-inl.h, spdlog automatically creates a stdout color logger using ansicolor_stdout_sink_mt on POSIX systems or wincolor_stdout_sink_mt on Windows. This logger receives the empty string name "" and is stored in both the loggers_ map and a dedicated default_logger_ member variable for fast access.
Fast Raw Pointer Access Pattern
Default API calls like spdlog::info() do not perform map lookups. Instead, they use registry::get_default_raw(), exposed publicly as spdlog::default_logger_raw(). This raw pointer access eliminates shared_ptr reference counting overhead, making the default API extremely fast. However, the source code in registry.h explicitly warns: you must not call set_default_logger() from one thread while another thread calls spdlog::info() or other default-API functions.
Safe Shared Pointer Retrieval
For scenarios requiring thread-safe access to the default logger object itself, use spdlog::default_logger() defined in include/spdlog/spdlog.h. This returns a std::shared_ptr<logger> to the current default instance, incrementing the reference count and ensuring the logger remains valid even if another thread replaces the default.
Replacing the Default Logger at Runtime
You can swap the default logger using spdlog::set_default_logger(new_logger), which forwards to registry::set_default_logger().
The replacement process follows these steps:
- Lock Acquisition: The registry acquires
logger_map_mutex_to ensure exclusive access - Map Insertion: The new logger is inserted into
loggers_using its name as the key (if not already present) - Pointer Update: The
default_logger_member is updated to point to the new instance
After replacement, subsequent calls to spdlog::info(), spdlog::debug(), and other default-API functions immediately operate on the new logger instance.
// Use the automatically initialized console logger
spdlog::info("Hello from default stdout logger");
// Extend the default logger with a file sink
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log", true);
spdlog::default_logger()->sinks().push_back(file_sink);
spdlog::info("Now logs to both console and file");
// Replace with a dedicated file logger
auto new_logger = spdlog::basic_logger_mt("file_logger", "output.log");
spdlog::set_default_logger(new_logger);
// After this point, spdlog::info() writes only to output.log
Thread Safety and Concurrency Considerations
While individual loggers are thread-safe, replacing the default logger requires careful synchronization. The raw pointer access used by the default API (spdlog::default_logger_raw()) is not atomic with respect to set_default_logger(). If you need to swap loggers at runtime, ensure no other threads are actively logging using the default API, or implement your own synchronization layer around logger replacement.
Summary
- The registry is a thread-safe singleton mapping string names to
std::shared_ptr<logger>instances, defined inregistry.handregistry-inl.h - Default logger is created automatically during registry construction with a color stdout sink, stored with the empty string key
"" - Zero-overhead access is provided via
default_logger_raw(), returning a raw pointer used byspdlog::info()and related macros - Safe access requires
default_logger()which returns ashared_ptrfor reference-counted safety - Runtime replacement uses
set_default_logger()but requires external synchronization to avoid race conditions with active logging calls - Global configuration stored in the registry propagates to all loggers via
initialize_logger()
Frequently Asked Questions
Where is the spdlog registry defined?
The registry class is declared in include/spdlog/details/registry.h and implemented in include/spdlog/details/registry-inl.h. You access the singleton instance via spdlog::details::registry::instance(), though most applications interact with it through the public API in include/spdlog/spdlog.h.
Is spdlog's default logger thread-safe?
Yes, the default logger is thread-safe because it uses mutex-protected sinks (ansicolor_stdout_sink_mt or wincolor_stdout_sink_mt). However, the act of replacing the default logger via set_default_logger() is not synchronized with the raw pointer reads performed by spdlog::info() and similar functions, creating a potential race condition during swaps.
How do I access the default logger without performance overhead?
Use spdlog::default_logger_raw() to obtain a raw pointer to the current default logger. The default API macros (SPDLOG_INFO, spdlog::info(), etc.) use this internally to avoid shared_ptr reference counting overhead during each log call, providing near-zero-cost abstraction for the common logging path.
What happens to the old default logger when I call set_default_logger?
The registry updates its internal default_logger_ pointer to the new instance and inserts the new logger into the loggers_ map using its name. The previous default logger remains alive if other shared_ptr instances reference it, or if it remains registered in the map under its original name. The registry does not automatically remove the old logger from loggers_ unless you explicitly call spdlog::drop() with its name.
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 →