How to Use spdlog's Pattern Formatter with Custom Flags

Implement custom pattern flags in spdlog by inheriting from spdlog::custom_flag_formatter, overriding the format() and clone() methods, and registering the flag with pattern_formatter::add_flag().

The gabime/spdlog library provides a flexible logging framework where log line formatting is handled by the pattern_formatter class. While spdlog ships with built-in flags like %l for log level and %v for the message, you can extend the formatter with custom flags to inject application-specific data, specialized time formats, or dynamic content. This guide demonstrates how to create, register, and use custom pattern flags based on the actual implementation in the spdlog v1.x source code.

Understanding the Pattern Formatter Architecture

The formatting pipeline centers on three key components defined in include/spdlog/pattern_formatter.h.

flag_formatter is the abstract base class that all formatters inherit. It receives a log message, a std::tm time structure, and a destination buffer, then writes its output to that buffer.

custom_flag_formatter extends flag_formatter and adds a pure-virtual clone() method. This method is required for deep-copying when a formatter instance is cloned (e.g., when creating new logger instances).

pattern_formatter::add_flag<T>() registers your custom formatter. This template method stores a std::unique_ptr<T> in an internal custom_handlers_ map, using a single character as the key. When set_pattern() parses a pattern string like %x, it checks this map for custom handlers before falling back to built-in flags.

When a log record is emitted, pattern_formatter::format iterates over the vector of formatter objects and calls each formatter's format method. Custom formatters participate in the same pipeline as built-in ones and can leverage spdlog's padding, truncation, and timezone facilities.

Creating a Custom Flag Formatter

Follow these steps to implement and register a custom flag.

Step 1: Inherit from custom_flag_formatter

Create a class that inherits from spdlog::custom_flag_formatter. You must implement two methods: format() to write output, and clone() to support copying.

Step 2: Implement the format Method

The format method signature is:

void format(const spdlog::details::log_msg& msg, 
            const std::tm& tm, 
            spdlog::memory_buf_t& dest) override;

Write your formatted output directly to the dest buffer using dest.append().

Step 3: Implement the clone Method

Return a new instance of your formatter with the same state:

std::unique_ptr<spdlog::custom_flag_formatter> clone() const override {
    return spdlog::details::make_unique<your_class>(/* args */);
}

Step 4: Register and Use the Flag

Instantiate a pattern_formatter, call add_flag<YourClass>('x', args...), then set the pattern using %x:

auto fmt = std::make_shared<spdlog::pattern_formatter>();
fmt->add_flag<your_class>('x', constructor_args)
    .set_pattern("[%n] %x %v");

Practical Examples

Simple Custom Flag (Constant String)

This example creates a flag %h that outputs a constant greeting string.

// custom_flag.hpp
#pragma once
#include <spdlog/custom_flag_formatter.h>

class hello_flag : public spdlog::custom_flag_formatter {
public:
    explicit hello_flag(std::string txt) : txt_(std::move(txt)) {}

    std::unique_ptr<spdlog::custom_flag_formatter> clone() const override {
        return spdlog::details::make_unique<hello_flag>(txt_);
    }

    void format(const spdlog::details::log_msg&, const std::tm&, 
                spdlog::memory_buf_t& dest) override {
        dest.append(txt_.data(), txt_.data() + txt_.size());
    }

private:
    std::string txt_;
};
// main.cpp
#include <spdlog/spdlog.h>
#include "custom_flag.hpp"

int main() {
    auto fmt = std::make_shared<spdlog::pattern_formatter>();
    fmt->add_flag<hello_flag>('h', "👋 Hello")
        .set_pattern("[%n] [%h] %v");

    auto logger = spdlog::stdout_logger_mt("demo");
    logger->set_formatter(fmt);
    logger->info("world");
}

Output:


[demo] [👋 Hello] world

Time Formatting Flag (12-Hour Notation)

This flag %t formats the current time in 12-hour notation with AM/PM.

class time12_flag : public spdlog::custom_flag_formatter {
public:
    std::unique_ptr<spdlog::custom_flag_formatter> clone() const override {
        return spdlog::details::make_unique<time12_flag>();
    }

    void format(const spdlog::details::log_msg&, const std::tm& tm,
                spdlog::memory_buf_t& dest) override {
        auto formatted = spdlog::fmt_lib::format("{:d}:{:02d}{}",
            tm.tm_hour % 12 == 0 ? 12 : tm.tm_hour % 12,
            tm.tm_min,
            tm.tm_hour >= 12 ? "PM" : "AM");
        dest.append(formatted.data(), formatted.data() + formatted.size());
    }
};

Usage:

auto fmt = std::make_shared<spdlog::pattern_formatter>();
fmt->add_flag<time12_flag>('t')
    .set_pattern("[%n] %t > %v");

Result (assuming local time is 14:05):


[demo] 2:05PM > Hello world

Padding and Truncation Support

The base class provides a padinfo_ member of type padding_info. When the pattern contains width specifiers like %5x or %=10x, spdlog populates this member automatically.

class padded_flag : public spdlog::custom_flag_formatter {
public:
    explicit padded_flag(std::string txt) : txt_(std::move(txt)) {}

    std::unique_ptr<spdlog::custom_flag_formatter> clone() const override {
        return spdlog::details::make_unique<padded_flag>(txt_);
    }

    void format(const spdlog::details::log_msg&, const std::tm&, 
                spdlog::memory_buf_t& dest) override {
        if (padinfo_.enabled()) {
            std::string padded(txt_.size() + padinfo_.width_, ' ');
            if (padinfo_.side_ == spdlog::details::padding_info::pad_side::right) {
                padded.replace(0, txt_.size(), txt_);
            } else {
                padded.replace(padded.size() - txt_.size(), txt_.size(), txt_);
            }
            dest.append(padded.data(), padded.data() + padded.size());
        } else {
            dest.append(txt_.data(), txt_.data() + txt_.size());
        }
    }

private:
    std::string txt_;
};
auto fmt = std::make_shared<spdlog::pattern_formatter>();
fmt->add_flag<padded_flag>('p', "data")
    .set_pattern("[%n] [%5p] %v");  // Produces: [demo] [ data] message

Error Handling in Custom Flags

Custom flags can throw exceptions to signal errors. The pattern_formatter propagates these as spdlog::spdlog_ex exceptions to the caller.

class exploding_flag : public spdlog::custom_flag_formatter {
public:
    explicit exploding_flag(std::string trigger) : trigger_(std::move(trigger)) {}

    std::unique_ptr<spdlog::custom_flag_formatter> clone() const override {
        return spdlog::details::make_unique<exploding_flag>(trigger_);
    }

    void format(const spdlog::details::log_msg&, const std::tm&, 
                spdlog::memory_buf_t&) override {
        if (trigger_ == "boom") {
            throw spdlog::spdlog_ex("exploding_flag triggered");
        }
    }

private:
    std::string trigger_;
};

The test suite in tests/test_pattern_formatter.cpp validates this behavior under the "custom flags-exception" section.

Summary

  • Inherit from custom_flag_formatter to create new pattern flags, implementing both format() for output generation and clone() for deep-copy support.
  • Register flags using pattern_formatter::add_flag<T>('x', args...) where 'x' becomes the pattern character (e.g., %x).
  • Access padding through the protected padinfo_ member to support width specifiers like %5x or %-10x.
  • Reference the source in include/spdlog/pattern_formatter.h for the class definitions and tests/test_pattern_formatter.cpp for comprehensive usage examples.
  • Handle errors by throwing spdlog::spdlog_ex exceptions within the format() method, which the library propagates to the caller.

Frequently Asked Questions

What is the difference between flag_formatter and custom_flag_formatter?

flag_formatter is the internal abstract base class used by all formatters, while custom_flag_formatter is the public extension point for user-defined flags. The key distinction is that custom_flag_formatter requires a clone() method to support deep-copying when formatters are duplicated across logger instances. Always inherit from custom_flag_formatter when adding new flags to ensure proper copy semantics.

How do I handle padding in custom pattern flags?

Access the padinfo_ protected member inherited from custom_flag_formatter. This padding_info structure contains the width, side (left/right/center), and truncate settings parsed from patterns like %5x or %-10.8x. Check padinfo_.enabled() to determine if padding was requested, then apply spaces or truncation accordingly before appending to the destination buffer.

Can custom flags access the log message content?

Yes. The format() method receives a const spdlog::details::log_msg& parameter containing the raw message text, log level, source location, and timestamp. You can access msg.payload for the message content, msg.level for the severity, or msg.source for filename and line number information, enabling context-sensitive formatting logic.

Where are custom flags stored in the formatter?

Custom flags are stored in the custom_handlers_ map inside pattern_formatter, defined in include/spdlog/pattern_formatter.h. This map associates single characters (the flag letters) with factory functions that create instances of your custom formatter. When set_pattern() parses a pattern string, it checks this map first before consulting the built-in flag registry, allowing your custom implementations to override or extend default behavior.

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 →