How Mapped Diagnostic Context (MDC) Works in spdlog: Thread-Local Logging Guide

spdlog's Mapped Diagnostic Context (MDC) provides thread-local key-value storage that automatically injects diagnostic values into log messages via the %MDC{key} pattern placeholder.

The Mapped Diagnostic Context (MDC) in spdlog enables automatic attachment of contextual metadata—such as request IDs or user sessions—to log entries without modifying every logging call. This lightweight facility stores data in thread-local storage, ensuring isolated contexts per thread with zero synchronization overhead.

What is Mapped Diagnostic Context (MDC)?

Mapped Diagnostic Context is a diagnostic logging pattern that maintains a per-thread map of contextual data. In spdlog, the MDC implementation lives in spdlog::detail::mdc and is exposed through the public header include/spdlog/mdc.h. Unlike traditional logging where you must pass context variables explicitly to each log function, MDC allows you to set values once per thread and have them automatically appear in formatted output.

Thread-Local Storage Architecture

The spdlog MDC implementation relies on thread-local storage to maintain independent context maps for each thread. According to the source code in include/spdlog/mdc.h, each thread maintains its own isolated map, eliminating the need for locks or synchronization primitives. This design ensures that context set in one thread never bleeds into another, making it ideal for multi-threaded server applications handling concurrent requests.

Core MDC API Methods

The public API exposed in include/spdlog/mdc.h provides four primary operations for managing thread-local context:

  • spdlog::set_mdc(key, value) – Stores a string value associated with the specified key in the current thread's context map.
  • spdlog::get_mdc(key) – Retrieves the value for a given key as an optional<std::string>, returning std::nullopt if the key does not exist.
  • spdlog::remove_mdc(key) – Removes a specific key-value pair from the current thread's MDC map.
  • spdlog::clear_mdc() – Clears all key-value pairs from the current thread's context, useful for cleanup at thread exit.

Pattern Formatter Integration

The integration between MDC and log output occurs in include/spdlog/pattern_formatter.h. When your log pattern contains the %MDC{key} placeholder, the formatter queries the current thread's MDC map for that specific key during message formatting. If the key exists, its value replaces the placeholder; if absent, the placeholder expands to an empty string. This resolution happens automatically whenever a log message is formatted through the logger.

Practical Code Examples

Basic MDC Usage

The following example demonstrates setting, retrieving, and clearing MDC values with pattern formatting:

#include <spdlog/spdlog.h>
#include <spdlog/mdc.h>

int main()
{
    // Create a logger (console sink for illustration)
    auto logger = spdlog::stdout_color_mt("example");

    // Define a pattern that prints the MDC entry "req_id"
    logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%MDC{req_id}] %v");

    // No MDC entry yet – placeholder will be empty
    logger->info("Started processing");

    // Set a request‑id for this thread
    spdlog::set_mdc("req_id", "42a7f9c3");

    logger->info("Handling request");           // → "[... ] [info] [42a7f9c3] Handling request"

    // Change the MDC value later on
    spdlog::set_mdc("req_id", "e9d2a1b0");
    logger->info("Finished request");           // → "[... ] [info] [e9d2a1b0] Finished request"

    // Remove a specific key
    spdlog::remove_mdc("req_id");
    logger->info("After removal");              // → "[... ] [info] [] After removal"

    // Clear the whole map (useful at thread exit)
    spdlog::clear_mdc();
}

Multi-Threaded Server Implementation

In server applications, MDC eliminates the need to pass context parameters through every function call. Each thread maintains its own isolated context:

#include <spdlog/spdlog.h>
#include <spdlog/mdc.h>
#include <thread>

void handle_client(int client_id)
{
    // Attach client‑specific context to this thread
    spdlog::set_mdc("client_id", std::to_string(client_id));

    auto logger = spdlog::get("server");
    logger->info("Connected");
    // …do work…
    logger->info("Disconnected");
}

int main()
{
    auto logger = spdlog::stdout_color_mt("server");
    logger->set_pattern("[%H:%M:%S] [client=%MDC{client_id}] %v");

    std::thread t1(handle_client, 1);
    std::thread t2(handle_client, 2);
    t1.join();
    t2.join();
}

Each thread prints its own client_id without any lock contention or cross-thread interference.

Summary

  • Thread-local storage in include/spdlog/mdc.h ensures each thread maintains an independent MDC map with zero synchronization overhead.
  • Four core API functions—set_mdc, get_mdc, remove_mdc, and clear_mdc—manage the lifecycle of context data.
  • Pattern integration via %MDC{key} in include/spdlog/pattern_formatter.h automatically injects context values into log messages.
  • Missing keys result in empty string expansion, ensuring logs remain formatted even without context.
  • Automatic cleanup using clear_mdc() prevents memory leaks when threads terminate.

Frequently Asked Questions

What happens if an MDC key is not set in spdlog?

When the pattern formatter encounters %MDC{key} for a key that does not exist in the current thread's MDC map, it expands the placeholder to an empty string. This behavior ensures log messages remain properly formatted even when optional context data is unavailable, as implemented in include/spdlog/pattern_formatter.h.

Is MDC thread-safe in spdlog?

Yes, spdlog's MDC is inherently thread-safe because it utilizes thread-local storage. Each thread maintains its own isolated map in spdlog::detail::mdc, meaning no locks or mutexes are required when calling set_mdc, get_mdc, or remove_mdc. However, you must ensure that clear_mdc() is called before thread termination to prevent memory leaks.

How do I prevent memory leaks when using MDC?

Always call spdlog::clear_mdc() at the end of a thread's lifecycle or when the context is no longer needed. This function clears all key-value pairs from the thread-local map, releasing stored strings. In long-running applications with dynamic thread pools, consider using RAII wrappers around set_mdc to ensure automatic cleanup via clear_mdc() or remove_mdc().

Can I use MDC with spdlog's asynchronous logging?

Yes, MDC works with asynchronous logging, but with important caveats. Since MDC is thread-local, the context must be set on the thread that calls the logging function. If you use asynchronous sinks where the formatting occurs on a different thread, the MDC context from the original calling thread will not automatically transfer. For async scenarios, either use synchronous logging or manually pass context through the message content itself.

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 →