How to Add Custom Data to spdlog Messages Using the Mapped Diagnostic Context
Use spdlog's built-in Mapped Diagnostic Context (MDC) API to attach thread-local key-value pairs that automatically append to every log message from the current thread until explicitly cleared.
The gabime/spdlog library provides a robust mechanism for adding custom data to spdlog messages without modifying individual logging calls. By leveraging the Mapped Diagnostic Context (MDC), you can enrich log records with contextual metadata—such as request IDs, user sessions, or transaction IDs—that persists across the current thread's execution lifecycle. This data resides in thread-local storage, ensuring isolation between concurrent threads while remaining implicitly available to all loggers.
Understanding the MDC Architecture
The MDC implementation in spdlog relies on thread-local storage (TLS) to maintain a per-thread map of diagnostic data. When TLS is enabled (the default unless compiled with SPDLOG_NO_TLS), the library stores your custom context in thread_local variables, guaranteeing that each thread maintains its own isolated diagnostic state.
The core API resides in include/spdlog/mdc.h, which exposes static methods for manipulating the context map. The actual formatting logic that injects this data into log lines lives in include/spdlog/pattern_formatter-inl.h (specifically the mdc_formatter implementation around lines 807-830). This formatter activates when your pattern string contains the MDC flag (%@ or %m depending on the specific version), expanding the stored key-value pairs into the final output.
Step-by-Step: Adding Custom Data to spdlog Messages
Putting Data Into the MDC
To associate custom data with the current thread, use spdlog::mdc::put(). This function accepts a key string and a value string, storing them in the thread-local map. Any subsequent log calls from this thread will automatically include this context.
#include <spdlog/mdc.h>
// Store request-specific metadata
spdlog::mdc::put("request_id", "abc-123");
spdlog::mdc::put("user_id", "42");
spdlog::mdc::put("client_ip", "192.168.1.1");
Configuring the Pattern Formatter
You must configure your logger's pattern to include the MDC output position. The formatter scans for the appropriate flag (typically %@ or %m) and substitutes the serialized MDC map at that location. The default pattern includes MDC support automatically when TLS is available.
#include <spdlog/spdlog.h>
// Pattern includes the MDC flag %@ followed by an identifier label
spdlog::set_pattern("%Y-%m-%d %H:%M:%S.%e [%n] [%l] %v %@[mdc]");
Managing Context Lifecycle
Remove individual keys using spdlog::mdc::erase() when specific data expires, or clear the entire context with spdlog::mdc::clear() at the end of a request or transaction to prevent data leakage into subsequent operations.
// Remove a specific key
spdlog::mdc::erase("request_id");
// Clear all thread-local context data
spdlog::mdc::clear();
Complete Working Implementation
This example demonstrates the full workflow: initializing the logger with an MDC-aware pattern, injecting custom data, logging messages that automatically include the context, and cleaning up when finished.
#include <spdlog/spdlog.h>
#include <spdlog/mdc.h>
int main() {
// Configure console logger with pattern that includes MDC output
spdlog::set_pattern("%Y-%m-%d %H:%M:%S.%e [%n] [%l] [%!:%#] [%^%v%$] %@[mdc]");
// Add custom data that will appear in every log from this thread
spdlog::mdc::put("request_id", "abc-123");
spdlog::mdc::put("user_id", "42");
spdlog::info("Started processing"); // → includes request_id and user_id
// Update values dynamically during execution
spdlog::mdc::put("user_id", "99");
spdlog::warn("User switched"); // → shows updated user_id
// Remove a specific key when no longer relevant
spdlog::mdc::erase("request_id");
spdlog::debug("Request ID removed"); // request_id omitted from output
// Clear entire context before thread exits or handles new request
spdlog::mdc::clear();
spdlog::info("Cleanup complete"); // No MDC data attached
}
Key Source Files and Implementation Details
According to the gabime/spdlog source code, these files define the MDC behavior:
include/spdlog/mdc.h: Defines the public API (put,erase,clear,get_context) and the thread-local storage container for the context map.include/spdlog/pattern_formatter-inl.h(lines 807-830): Implementsmdc_formatter, the class responsible for serializing the MDC map into string form when processing log patterns.include/spdlog/spdlog.h: Provides theset_pattern()entry point and initializes default patterns that respect theSPDLOG_NO_TLSpreprocessor definition.include/spdlog/logger.h: Core logger implementation that coordinates between theloggerclass and the active formatter to retrieve and output the final formatted message.
Summary
- Thread-local isolation: MDC data in spdlog is stored per-thread, preventing cross-contamination between concurrent logging contexts.
- Zero call-site modification: Once configured, you add custom data to spdlog messages by manipulating the MDC map rather than changing every
spdlog::info()orspdlog::debug()call. - Pattern-driven output: The
mdc_formatterinpattern_formatter-inl.hhandles the actual string expansion when your pattern includes the MDC flag. - Explicit cleanup: Always call
spdlog::mdc::clear()at transaction boundaries to ensure context data does not persist unintentionally.
Frequently Asked Questions
What is the performance impact of using MDC in spdlog?
The performance overhead is minimal and consists primarily of a hash map lookup and string serialization. Because the data lives in thread-local storage (avoiding locks), reads and writes operate at memory speed with no synchronization costs between threads. The mdc_formatter only executes when a log message is actually produced, so inactive logging levels incur zero MDC processing overhead.
Can I use MDC without TLS support?
No. The MDC mechanism explicitly requires thread-local storage to maintain context isolation. If you compile spdlog with SPDLOG_NO_TLS defined, the mdc.h header and its associated formatter in pattern_formatter-inl.h are effectively disabled, and attempts to use spdlog::mdc::put() will result in compilation errors or undefined behavior depending on your include structure.
How do I format MDC output differently than the default?
The mdc_formatter class in pattern_formatter-inl.h controls the default serialization, which typically outputs key-value pairs in a simple format. To customize the output, you can implement a custom spdlog::custom_flag_formatter that reads the MDC context via spdlog::mdc::get_context() and formats it according to your needs (such as JSON or pipe-delimited strings), then register that formatter with your logger's pattern using a custom flag character.
Is MDC data shared between threads?
No. By design, MDC data is strictly thread-local. Each thread maintains its own independent map initialized via spdlog::mdc::put(). Data does not propagate to child threads or worker pools automatically. If you need to transfer context across thread boundaries, you must manually extract the data using spdlog::mdc::get_context() in the parent thread and re-inject it via spdlog::mdc::put() in the child thread before 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 →