Creating Custom Pattern Flags with spdlog's PatternFormatter: A Complete Implementation Guide

You can extend spdlog's formatting capabilities by inheriting from spdlog::custom_flag_formatter, implementing the required clone() and format() methods, and registering your custom flag with pattern_formatter::add_flag() to use it in pattern strings alongside built-in flags like %l and %v.

The gabime/spdlog library formats log lines through the pattern_formatter class, which parses pattern strings into a vector of flag_formatter objects. While spdlog ships with standard flags for timestamps, log levels, and messages, the architecture allows you to inject custom formatting logic by extending the custom_flag_formatter base class defined in include/spdlog/pattern_formatter.h.

How spdlog Pattern Formatting Works

When a log record is emitted, pattern_formatter::format iterates over an internal vector of formatter objects (formatters_) and calls each formatter's format method. The parsing happens when you call set_pattern, which resolves both built-in flags and user-registered custom flags.

The key components involved in custom flag creation include:

  • spdlog::details::flag_formatter — The abstract base class that receives a log message, a std::tm time structure, and a destination buffer.
  • spdlog::custom_flag_formatter — Extends flag_formatter and adds a pure-virtual clone() method required for deep-copying when formatters are duplicated.
  • pattern_formatter::add_flag<T>(char flag, Args&&…args) — Stores a std::unique_ptr<T> in the internal custom_handlers_ map, where T must derive from custom_flag_formatter. The character key (flag) is what you use in your pattern string (e.g., %x).
  • custom_handlers_ — A map inside pattern_formatter that associates flag characters with factory functions for creating formatter instances.

Custom formatters participate in the same pipeline as built-in ones and can leverage the same padding, truncation, and timezone facilities that spdlog provides.

Step-by-Step: Creating a Custom Pattern Flag

Step 1: Inherit from custom_flag_formatter

Create a class that inherits from spdlog::custom_flag_formatter and implement the two pure virtual methods: clone() and format().

#include <spdlog/pattern_formatter.h>

class hello_flag : public spdlog::custom_flag_formatter {
public:
    explicit hello_flag(std::string txt) : txt_(std::move(txt)) {}
    
    // Required implementation
    std::unique_ptr<spdlog::custom_flag_formatter> clone() const override;
    void format(const spdlog::details::log_msg&, const std::tm&, spdlog::memory_buf_t& dest) override;
    
private:
    std::string txt_;
};

Step 2: Implement the clone() Method

The clone() method enables spdlog to deep-copy your formatter when the pattern formatter is duplicated. Use spdlog::details::make_unique to maintain compatibility with the library's memory management.

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

Step 3: Implement the format() Method

The format() method receives the log message details, the time structure, and a reference to the destination buffer. Write your custom output directly to this buffer.

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

Step 4: Register with pattern_formatter

Instantiate a pattern_formatter and call add_flag<T>() with your flag character and any constructor arguments, then set the pattern using your new flag.

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

Practical Examples

Example 1: 12-Hour Time Format Flag

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

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

Example 2: Supporting Padding and Truncation

The base class custom_flag_formatter inherits the padinfo_ member of type padding_info. When your pattern includes width specifiers like %5x or %-10x, spdlog populates this structure. You can manually apply padding or use the base class utilities.

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_;
};

Usage:

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

Example 3: Error Handling with Exceptions

Custom flags can throw exceptions during formatting, which spdlog propagates as spdlog::spdlog_ex. This implementation triggers an error when a specific condition is met.

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_;
};

When used with add_flag<exploding_flag>('e', "boom"), calling format throws a spdlog::spdlog_ex that propagates to the caller, as demonstrated in the test suite.

Key Source Files and Implementation Details

The implementation of custom pattern flags spans the following files in the gabime/spdlog repository:

The add_flag template stores your custom formatter in the custom_handlers_ map using perfect forwarding of constructor arguments, enabling flexible initialization of your flag objects.

Summary

  • Inherit from spdlog::custom_flag_formatter to create custom pattern flags that integrate with spdlog's formatting pipeline.
  • Implement clone() to enable deep-copying of your formatter, using spdlog::details::make_unique for consistency.
  • Implement format() to write custom output to the provided memory_buf_t buffer using the log message and time data.
  • Register with add_flag<T>() on a pattern_formatter instance, specifying the flag character and constructor arguments.
  • Leverage padinfo_ to support standard spdlog padding and alignment specifiers in your custom flags.
  • Handle exceptions carefully, as errors thrown in format() propagate as spdlog::spdlog_ex.

Frequently Asked Questions

What is the difference between flag_formatter and custom_flag_formatter?

The flag_formatter class is the internal abstract base used by all formatters, while custom_flag_formatter extends it specifically for user-defined flags by adding the pure-virtual clone() method. You must inherit from custom_flag_formatter (not flag_formatter) when creating custom flags to ensure proper deep-copy semantics when the pattern formatter is cloned.

How do I handle padding and alignment in my custom flag?

The custom_flag_formatter base class provides a protected member padinfo_ of type padding_info. When users specify width in the pattern (e.g., %8x or %-5x), spdlog automatically populates this structure. You can check padinfo_.enabled() to determine if padding is required, and access padinfo_.width_ and padinfo_.side_ to apply left, right, or center alignment manually in your format() implementation.

Can I register multiple custom flags with the same pattern formatter?

Yes. The pattern_formatter::add_flag() method returns a reference to the formatter instance, allowing chained registration calls. Each custom flag must use a unique character identifier, and you can mix custom flags with built-in ones in the same pattern string.

What happens if my custom flag throws an exception during formatting?

If your custom flag's format() method throws an exception, spdlog catches it and rethrows as a spdlog::spdlog_ex. This exception propagates up through the logging call stack, potentially terminating the application if not handled. The test suite in tests/test_pattern_formatter.cpp includes specific test cases for exception handling in custom flags to ensure this behavior remains consistent across versions.

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 →