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 viaos::now().thread_id(size_t): Operating system thread identifier (omitted whenSPDLOG_NO_THREAD_IDis 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:
my_logger.info()constructs alog_msgwith payload"Hello world!".- The default pattern formatter populates timestamp metadata in
msg.payloadand calculatescolor_rangeoffsets. base_sink<std::mutex>::log(msg)acquires the lock and forwards tobasic_file_sink::sink_it_(msg).- The file sink writes the final formatted message to
example.log.
Essential Source Files
Understanding the log_msg lifecycle requires examining these specific files:
include/spdlog/details/log_msg.h– Structure definition and member layout.include/spdlog/details/log_msg-inl.h– Constructors initializing timestamp, thread ID, and source location.include/spdlog/sinks/base_sink.h– Abstract template base class declaring the finallog()method.include/spdlog/sinks/base_sink-inl.h– Implementation of thread-safe forwarding tosink_it_.include/spdlog/pattern_formatter.h– Logic for mutatingpayloadand setting color ranges.include/spdlog/sinks/basic_file_sink.h– Reference implementation of a concrete sink.include/spdlog/logger.h– Orchestrateslog_msgconstruction and multi-sink dispatch.
Summary
log_msgis an immutable aggregate struct defined ininclude/spdlog/details/log_msg.hthat holds all metadata required for logging.- The struct contains
logger_name,level,time,thread_id,source,payload, and mutablecolor_rangeoffsets. - Loggers instantiate one
log_msgper logging call using constructors fromlog_msg-inl.h. - Messages propagate through the pipeline by const-reference via
base_sink::log(), which locks the mutex and calls the virtualsink_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →