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): Implements mdc_formatter, the class responsible for serializing the MDC map into string form when processing log patterns.
  • include/spdlog/spdlog.h: Provides the set_pattern() entry point and initializes default patterns that respect the SPDLOG_NO_TLS preprocessor definition.
  • include/spdlog/logger.h: Core logger implementation that coordinates between the logger class 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() or spdlog::debug() call.
  • Pattern-driven output: The mdc_formatter in pattern_formatter-inl.h handles 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:

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 →