How to Use Custom Pattern Flags in spdlog: A Complete Guide

Create a class inheriting from spdlog::custom_flag_formatter, implement the virtual format() and clone() methods, and register the flag using pattern_formatter::add_flag() to use custom pattern flags in spdlog formatting.

spdlog formats log lines through the pattern_formatter class, which expands pattern strings like "%l %v" using internal flag formatters. While the library ships with built-in flags for timestamps, log levels, and source locations, you can extend the system with custom pattern flags to inject domain-specific context—such as request IDs, thread names, or custom timestamp formats—directly into your log output. This guide demonstrates how to implement and register these extensions using the actual source code from the gabime/spdlog repository.

Understanding the spdlog Pattern Formatting Architecture

The formatting pipeline in spdlog relies on three key components defined in include/spdlog/pattern_formatter.h. The flag_formatter abstract base class defines the interface for all pattern tokens, requiring a format() method that receives the log message, a std::tm time structure, and a destination buffer. The custom_flag_formatter class extends this interface by adding a pure-virtual clone() method, which enables deep-copying when formatters are duplicated. Finally, the pattern_formatter::add_flag<T>() template method registers your custom class in an internal custom_handlers_ map, associating a single character (like 'x') with your formatter implementation.

When you call set_pattern() on a formatter instance, spdlog parses the pattern string and builds a vector of formatter objects. Custom flags participate in this pipeline identically to built-in flags, supporting the same padding, truncation, and time-zone facilities provided by the library.

Creating a Custom Pattern Flag

Implementing custom pattern flags in spdlog requires three distinct steps: defining the formatter class, registering it with the pattern formatter, and applying the formatter to a logger.

Step 1: Inherit from custom_flag_formatter

Create a class that derives from spdlog::custom_flag_formatter and implement both the format() method and the clone() method. The format() method writes your custom output into the provided memory_buf_t buffer, while clone() ensures spdlog can copy your formatter when creating new logger instances.

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

Step 2: Register the Flag with add_flag()

Instantiate a pattern_formatter and use the add_flag<T>() template method to bind your custom flag class to a specific pattern character. This method stores a std::unique_ptr<T> in the formatter's internal registry.

auto fmt = std::make_shared<spdlog::pattern_formatter>();
fmt->add_flag<hello_flag>('h', "👋 Hello");

Step 3: Set the Pattern and Apply to Logger

Use set_pattern() to include your custom flag (referenced by % followed by your registered character) in the format string, then assign the formatter to a logger.

fmt->set_pattern("[%n] [%h] %v");

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

This produces the output:

[demo] [👋 Hello] world

Advanced Custom Pattern Flag Examples

The following examples demonstrate practical implementations found in the spdlog test suite at tests/test_pattern_formatter.cpp, showing how to handle time formatting, padding, and error conditions.

Custom Time Formatting (12-Hour Clock)

This flag formats the current time in 12-hour notation with AM/PM markers, accessing the std::tm structure passed to the format() method.

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");

Assuming the local time is 14:05, this outputs:

[demo] 2:05PM > Hello world

Handling Padding and Alignment

Custom flags automatically respect padding specifiers like %8x or %=10x through the protected padinfo_ member inherited from the base class. The padding_info structure contains the width, alignment side, and truncation settings.

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

When used with a pattern like [%5p], this produces right-padded output such as [ data].

Error Handling and Exceptions

Custom flags may throw exceptions during formatting. When a flag's format() method throws spdlog::spdlog_ex (or any exception), the pattern_formatter::format method propagates this to the caller. The test suite verifies this behavior in the custom flags‑exception section of tests/test_pattern_formatter.cpp.

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

Key Implementation Files

According to the gabime/spdlog source code, the following files define the custom flag API:

Summary

  • Inherit from spdlog::custom_flag_formatter to create new pattern flags, implementing both format() for output generation and clone() for deep-copying.
  • Register flags using pattern_formatter::add_flag<T>('x', args...), where 'x' becomes the pattern character accessed via %x.
  • Support padding by checking the padinfo_ member inherited from the base class, which spdlog populates when patterns like %5x or %-10x are used.
  • Reference the source in include/spdlog/pattern_formatter.h and tests/test_pattern_formatter.cpp for the complete API specification and test patterns.

Frequently Asked Questions

What is the difference between flag_formatter and custom_flag_formatter in spdlog?

The flag_formatter class in include/spdlog/pattern_formatter.h is the abstract base that defines the format() interface used by all pattern tokens. The custom_flag_formatter class extends this base by adding a pure-virtual clone() method, which spdlog requires to deep-copy formatter objects when duplicating logger configurations. You must inherit from custom_flag_formatter (not flag_formatter) when creating user-defined flags so that the pattern_formatter can properly clone your custom handlers.

How do I handle padding and alignment in custom spdlog pattern flags?

Your custom flag class inherits a protected member called padinfo_ of type spdlog::details::padding_info from the custom_flag_formatter base. When users specify width in the pattern (e.g., %8x for right-align or %-8x for left-align), spdlog automatically parses these specifiers and populates padinfo_ before calling your format() method. You can check padinfo_.enabled() to determine if padding is required, then use padinfo_.width_ and padinfo_.side_ to format your output accordingly.

Can custom pattern flags in spdlog throw exceptions?

Yes. If your custom flag's format() method throws an exception (such as spdlog::spdlog_ex), the pattern_formatter::format method will propagate that exception to the caller. This behavior is verified in tests/test_pattern_formatter.cpp under the custom flags exception tests. You should handle recoverable errors internally if you want logging to continue, or throw exceptions to signal critical failures that should halt processing.

How do I register multiple custom flags on the same pattern formatter?

Call add_flag<T>() multiple times on the same pattern_formatter instance, passing a unique character for each flag. Each invocation stores a std::unique_ptr to your formatter in the internal custom_handlers_ map. For example:

auto fmt = std::make_shared<spdlog::pattern_formatter>();
fmt->add_flag<request_id_flag>('r', get_request_id());
fmt->add_flag<session_flag>('s', session_token);
fmt->set_pattern("[%r] [%s] %v");

This allows you to combine multiple custom pattern flags in a single log format string.

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 →