How to Implement Custom Formatter Flags for spdlog Pattern Formatter

Create a class inheriting from spdlog::custom_flag_formatter, implement the format() and clone() methods, then register it with pattern_formatter::add_flag() to use your custom flag in pattern strings.

The spdlog logging library provides a flexible pattern formatting system that allows you to customize log output through formatter flags. While it includes built-in flags for timestamps, log levels, and messages, you can extend this system by implementing custom formatter flags for spdlog pattern formatter to add application-specific data or formatting logic.

Understanding the spdlog Pattern Formatter Architecture

The spdlog formatting pipeline centers on the pattern_formatter class located in include/spdlog/pattern_formatter.h. When you call set_pattern(), the formatter parses the pattern string and constructs a vector of flag_formatter objects that transform log records into formatted output.

The flag_formatter Base Class

At the core of the system is spdlog::details::flag_formatter, an abstract base class defined in pattern_formatter.h. This class receives a log message (details::log_msg), a std::tm time structure, and a destination buffer (memory_buf_t), then writes its formatted output to that buffer.

The custom_flag_formatter Extension

To create user-defined flags, you inherit from spdlog::custom_flag_formatter, which extends flag_formatter with a pure-virtual clone() method. This clone method is required for deep-copying when a formatter is duplicated, ensuring thread safety and proper object lifecycle management.

Registration via add_flag

The pattern_formatter::add_flag<T>(char flag, Args&&... args) template method stores a std::unique_ptr<T> in the internal custom_handlers_ map. The character key (flag) becomes the pattern trigger recognized in format strings like %x. When set_pattern() parses the pattern, it resolves both built-in and user-registered flags to build the formatters_ vector.

Step-by-Step Implementation Guide

Step 1: Inherit from custom_flag_formatter

Create a class that inherits from spdlog::custom_flag_formatter and implement the required methods. You must override format() to write your custom output to the destination buffer, and clone() to return a new instance of your formatter.

#include <spdlog/pattern_formatter.h>

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

    void format(const spdlog::details::log_msg&, const std::tm&, 
                spdlog::memory_buf_t& dest) override {
        // Write your custom output to dest
        dest.append("custom", 6);
    }
};

Step 2: Register with pattern_formatter

Instantiate a pattern_formatter and call add_flag() with your class type and the desired flag character. You can pass constructor arguments as variadic template parameters.

auto fmt = std::make_shared<spdlog::pattern_formatter>();
fmt->add_flag<my_custom_flag>('x');  // Registers %x

Step 3: Use in Pattern Strings

After registration, use your flag in the pattern string passed to set_pattern(). The custom flag participates in the formatting pipeline alongside built-in flags.

fmt->set_pattern("[%n] [%x] %v");
logger->set_formatter(fmt);

Practical Code Examples

Example 1: Simple Constant String Flag

This implementation creates a flag that inserts a constant string into the log output, useful for adding static metadata or separators.

// custom_flag.hpp
#pragma once
#include <spdlog/pattern_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

Example 2: 12-Hour Time Format Flag

This custom flag formats the current time in 12-hour notation with AM/PM indicators, extending spdlog's time formatting capabilities.

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());
    }
};
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 3: Flag with Padding and Truncation Support

The custom_flag_formatter base class exposes the padinfo_ member (type padding_info) that automatically populates when users specify width in patterns like %5x or %-10x. Access this member to honor padding and truncation specifications.

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

Output:


[demo] [ data] message

Example 4: Exception Handling in Custom Flags

Custom flags can throw exceptions during formatting, which propagate through the pattern_formatter::format method. The test suite in tests/test_pattern_formatter.cpp demonstrates this behavior for error handling validation.

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 registered with fmt->add_flag<exploding_flag>('e', "boom"), any log call triggers a spdlog::spdlog_ex exception that propagates to the caller.

Summary

  • Derive from custom_flag_formatter in include/spdlog/pattern_formatter.h to create new pattern flags
  • Implement clone() to enable deep-copying when formatters are duplicated
  • Implement format() to write output to the memory_buf_t destination using log message data and time structures
  • Register with add_flag<T>() to bind a character trigger to your formatter class
  • Access padinfo_ to support padding specifiers like %5x or %-10x in pattern strings
  • Reference tests/test_pattern_formatter.cpp for comprehensive examples of custom flag behavior, padding, and exception handling

Frequently Asked Questions

How do I access the log message content in a custom flag formatter?

The format() method receives a const spdlog::details::log_msg& parameter containing the log message payload, level, and other metadata. Access log_msg.payload to read the actual message text, or check log_msg.level to conditionally format based on severity.

Can I use constructor arguments when registering custom flags?

Yes, the add_flag<T>() method accepts variadic template arguments that forward to your formatter's constructor. For example: fmt->add_flag<my_flag>('x', "arg1", 42) constructs my_flag("arg1", 42) and stores it in the internal handlers map.

Where does spdlog store the mapping between flag characters and formatter objects?

The pattern_formatter class maintains a private custom_handlers_ member (a map of char to std::unique_ptr<custom_flag_formatter>) populated by add_flag() calls. When set_pattern() parses the pattern string, it looks up each % sequence in this map to instantiate the appropriate formatter, as implemented in include/spdlog/pattern_formatter.h.

Do custom flags support the same padding and truncation syntax as built-in flags?

Yes, when you inherit from custom_flag_formatter, you automatically inherit the padinfo_ member of type padding_info. This structure populates when users specify width (e.g., %5x), truncation (e.g., %.3x), or alignment (e.g., %-5x for left-align) in the pattern string, allowing your custom implementation to respect the same formatting specifications as built-in flags.

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 →