How spdlog's Pattern Formatter Parses Custom Format Strings Internally

spdlog parses pattern strings in a single pass through compile_pattern_, which delegates to handle_padspec_ for padding extraction and handle_flag_ for formatter instantiation, producing a vector of formatter objects stored in formatters_.

The spdlog library uses a sophisticated pattern formatter to transform format strings like "%Y-%m-%d %H:%M:%S.%e [%l] %v" into structured log output. Understanding how spdlog's pattern formatter parses custom format strings internally reveals why it achieves high performance while supporting extensive customization, including user-defined flags, padding specifications, and truncation behavior.

The Three-Stage Parsing Pipeline

When you call spdlog::set_pattern() or construct a pattern_formatter directly, the library immediately compiles the pattern string into an executable chain of formatter objects. This compilation happens in three coordinated stages within /include/spdlog/pattern_formatter-inl.h.

Stage 1: Pattern Compilation with compile_pattern_

The entry point is compile_pattern_(const std::string& pattern), which performs a character-by-character scan of the input string. As it iterates, the function distinguishes between literal text and format specifiers.

When the scanner encounters ordinary characters, it accumulates them into an aggregate_formatter that copies text verbatim to the output buffer. Upon reaching a % character, the function finalizes any pending aggregate formatter and transitions to padding analysis. This single-pass approach ensures minimal overhead during subsequent log operations.

Stage 2: Padding Specification Parsing with handle_padspec_

Immediately following a %, the parser calls handle_padspec_ to extract optional padding directives. This function reads the specification according to these rules:

  • Alignment: - indicates right alignment, = indicates center alignment, and default (no character) means left alignment.
  • Width: One or more digits specify the field width, capped internally at 64 characters.
  • Truncation: An optional ! character activates truncation mode, which cuts the output if it exceeds the specified width.

The function returns a padding_info struct containing these parameters, which determines whether the subsequent flag formatter will be wrapped in a scoped_padder (real padding) or null_scoped_padder (no padding) during the final formatting phase.

Stage 3: Flag Dispatch and Formatter Instantiation with handle_flag_

With padding information resolved, compile_pattern_ dispatches to handle_flag_<Padder>(). This template function performs a two-tier lookup:

  1. Custom Flag Check: It first searches the custom_handlers_ map for user-registered flags added via add_flag().
  2. Built-in Flag Resolution: If no custom handler exists, it switches over known flag characters (Y for year, m for month, v for message text, etc.) and instantiates the corresponding specialized formatter (e.g., details::Y_formatter<Padder>).

For date and time components, the function sets the need_localtime_ flag to ensure the formatter cache gets refreshed appropriately. Unknown flag characters are emitted verbatim unless truncation is active, in which case they are interpreted as source function names.

Runtime Execution of Compiled Formatters

After compilation, the formatters_ vector contains a sequence of ready-to-execute formatter objects. When pattern_formatter::format receives a log message, it iterates through this vector and invokes each formatter's format method, passing the log record, a std::tm structure, and the output buffer.

The scoped_padder wrapper handles the actual padding logic just before each formatter writes its output, prepending or appending spaces (or truncating content) according to the padding_info captured during parsing. This separation of parsing and execution allows spdlog to optimize the hot path—formatting millions of log messages—without re-parsing the pattern string.

Practical Implementation Examples

The following examples demonstrate how to leverage the pattern formatter's capabilities:

// Example 1: Default full formatter (equivalent to "%+")
spdlog::set_pattern("%+");
// Example 2: Custom timestamp with padding and truncation
// %8!v pads the message to 8 characters and truncates if longer
spdlog::set_pattern("%Y-%m-%d %H:%M:%S.%e [%^%l%$] %8!v");
// Example 3: Registering a user-defined custom flag
class my_flag : public spdlog::custom_flag_formatter {
public:
    std::unique_ptr<spdlog::custom_flag_formatter> clone() const override {
        return std::make_unique<my_flag>();
    }
    
    void format(const spdlog::details::log_msg&, 
                const std::tm&, 
                spdlog::memory_buf_t& dest) override {
        spdlog::details::fmt_helper::append_string_view("[myflag]", dest);
    }
};

auto fmt = std::make_shared<spdlog::pattern_formatter>("%v %^%l%$");
fmt->add_flag('X', my_flag{});  // Register %X as custom flag
spdlog::set_formatter(fmt);

Summary

  • Single-pass compilation: The compile_pattern_ function in pattern_formatter-inl.h transforms text patterns into executable formatter objects during construction.
  • Modular parsing: handle_padspec_ extracts padding metadata while handle_flag_ resolves flag characters to concrete formatter instances.
  • Extensible design: Custom flags integrate seamlessly through add_flag() and are resolved alongside built-in formatters like Y_formatter or v_formatter.
  • Efficient execution: Pre-compiled formatters stored in formatters_ eliminate parsing overhead during the actual logging hot path.

Frequently Asked Questions

What happens if I use an unknown flag character in my pattern string?

According to the implementation in /include/spdlog/pattern_formatter-inl.h, unknown flags are emitted verbatim to the output. For example, %x will appear as literal text unless you have registered a custom flag handler via add_flag(). The exception occurs when truncation is active—if the ! specifier precedes an unknown flag, spdlog interprets it as a source function name rather than a literal string.

How does padding affect performance in spdlog pattern formatting?

Padding incurs minimal overhead because the width calculation happens once during the compilation phase in handle_padspec_. At runtime, the scoped_padder wrapper calculates the required padding size by querying the formatter's output length, then uses efficient buffer operations from fmt_helper.h to inject spaces. If no padding is specified, the formatter uses null_scoped_padder, a zero-cost abstraction that compiles away entirely.

Can I modify the pattern formatter after it has been constructed?

While you cannot change the pattern string dynamically after construction, you can register additional custom flags at any time using add_flag(). However, doing so requires thread-safe access to the custom_handlers_ map. For complete pattern changes, spdlog recommends creating a new pattern_formatter instance and swapping it via set_formatter(), as the formatters_ vector is optimized for immutable execution.

What is the maximum width I can specify for padding operations?

The parser caps the width value at 64 characters during the handle_padspec_ execution. If you specify a width greater than 64, the parser truncates the value to this maximum to prevent excessive memory allocation and potential buffer overflow issues during the formatting phase.

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 →