spdlog log_msg Internal Structure: How Log Records Flow Through Sinks

spdlog constructs an immutable spdlog::details::log_msg object for every logging request and passes it by const-reference through the sink pipeline, ensuring zero-copy efficiency and consistent record delivery.

The spdlog library (gabime/spdlog) centralizes log metadata in a single lightweight structure called log_msg. Understanding the internal layout of this struct and its journey from logger to output destination is crucial for implementing high-performance custom sinks or debugging formatting issues.

Anatomy of the log_msg Structure

The log_msg definition resides in include/spdlog/details/log_msg.h, with constructors implemented in include/spdlog/details/log_msg-inl.h. This aggregate struct encapsulates everything a sink needs to render or forward a record.

Core Data Members

Each log_msg instance contains the following fields:

  • logger_name (string_view_t): Identifies the originating logger.
  • level (level::level_enum): Severity classification (trace, debug, info, warning, error, critical).
  • time (log_clock::time_point): Timestamp captured at construction via os::now().
  • thread_id (size_t): Operating system thread identifier (omitted when SPDLOG_NO_THREAD_ID is defined).
  • source (source_loc): Source code location containing __FILE__, __LINE__, and __func__.
  • payload (string_view_t): The raw or formatted message content.
  • color_range_start / color_range_end (size_t, mutable): Byte offsets indicating which portion of the payload should receive ANSI color codes.

Immutability and Thread Safety

All members are either fundamental value types or string_view_t references to external storage. This design makes the object effectively immutable after construction, allowing the logger to instantiate one log_msg and safely distribute const-references to multiple sinks without requiring per-message synchronization.

The Sink Pipeline: Passing log_msg by Const Reference

The path from logger invocation to final output follows a strict three-layer architecture defined in include/spdlog/logger.h and the sink headers.

Logger Construction and Formatting

When you invoke a logging method such as logger::info(), the logger constructs a log_msg and populates its fields:

// Conceptual flow inside spdlog::logger::log(...)
details::log_msg msg{logger_name_, lvl, fmt::format_to(...)};
msg.time = os::now();
msg.thread_id = os::thread_id();

The logger then executes the registered pattern formatter (defined in include/spdlog/pattern_formatter.h) which may mutate the payload string to include timestamps and set the color_range_start and color_range_end offsets for terminal colorization.

The base_sink Abstraction

Every thread-safe sink inherits from base_sink<Mutex> in include/spdlog/sinks/base_sink.h. The logger iterates over its vector of std::shared_ptr<sink> objects and invokes the non-virtual final method:

virtual void log(const details::log_msg &msg) final override;

The implementation in include/spdlog/sinks/base_sink-inl.h handles locking before dispatch:

void base_sink<Mutex>::log(const details::log_msg &msg)
{
    std::lock_guard<Mutex> lock{mutex_};
    sink_it_(msg);  // Pure virtual dispatch to concrete implementation
}

By passing const details::log_msg &, the pipeline eliminates copy overhead while guaranteeing that every sink receives an identical view of the log record.

Concrete Sink Implementation

Concrete sinks implement the pure virtual sink_it_(const details::log_msg &msg) method. For example, the file sink in include/spdlog/sinks/basic_file_sink-inl.h writes the payload directly:

void basic_file_sink<Mutex>::sink_it_(const details::log_msg &msg)
{
    // Write msg.payload to the open file stream
    file_helper_.write(msg.payload);
}

Console sinks such as stdout_color_sink access the color_range members to wrap specific payload segments with ANSI escape codes before writing to std::cout.

Practical Example: Creating and Routing log_msg

This complete example demonstrates the full lifecycle from logger invocation to file output:

#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>

int main()
{
    // 1. Create a thread-safe file sink
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("example.log");
    
    // 2. Build a logger attached to the sink
    spdlog::logger my_logger("my_logger", {file_sink});
    
    // 3. Emit a message
    my_logger.info("Hello {}!", "world");
}

Execution flow:

  1. my_logger.info() constructs a log_msg with payload "Hello world!".
  2. The default pattern formatter populates timestamp metadata in msg.payload and calculates color_range offsets.
  3. base_sink<std::mutex>::log(msg) acquires the lock and forwards to basic_file_sink::sink_it_(msg).
  4. The file sink writes the final formatted message to example.log.

Essential Source Files

Understanding the log_msg lifecycle requires examining these specific files:

Summary

  • log_msg is an immutable aggregate struct defined in include/spdlog/details/log_msg.h that holds all metadata required for logging.
  • The struct contains logger_name, level, time, thread_id, source, payload, and mutable color_range offsets.
  • Loggers instantiate one log_msg per logging call using constructors from log_msg-inl.h.
  • Messages propagate through the pipeline by const-reference via base_sink::log(), which locks the mutex and calls the virtual sink_it_() method.
  • This architecture eliminates message copying and ensures consistent, thread-safe delivery to all registered sinks.

Frequently Asked Questions

What makes spdlog's log_msg immutable?

All log_msg members are either fundamental types or string_view_t references to external storage. Once constructed by the logger, the struct is not modified except for the mutable color_range offsets used by pattern formatters. This immutability allows safe concurrent access by multiple sinks via const-reference without synchronization overhead on the message itself.

Why does base_sink use a template parameter for Mutex?

The base_sink<Mutex> template enables compile-time selection between thread-safe (std::mutex) and single-threaded (null_mutex) sink implementations. The log() method locks the specific mutex type before calling the virtual sink_it_(), providing zero-cost abstraction when thread safety is unnecessary while guaranteeing synchronization when it is required.

Can custom sinks modify the log message payload?

No. Concrete sinks receive the log_msg via const details::log_msg &msg in their sink_it_() implementation. While the struct contains mutable color_range members intended for formatter use, the payload itself is a string_view_t and should be treated as read-only to maintain consistency across multiple sinks receiving the same message reference.

How does the color_range information reach terminal sinks?

The color_range_start and color_range_end members specify byte offsets within msg.payload that should be wrapped with ANSI color codes. During construction, the pattern formatter calculates these offsets based on format specifiers like %^ and %$. Console sinks such as stdout_color_sink read these values to apply terminal colors, while file sinks typically ignore them and write the raw payload.

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 →