How to Implement Custom Formatter Flags in spdlog's Pattern Matching System
Derive from spdlog::custom_flag_formatter, implement format() and clone(), then register with pattern_formatter::add_flag<T>() to define custom pattern flags like %x.
spdlog's pattern matching system transforms log format strings into sequences of flag formatters executed per record. While built-in flags like %v (message) and %l (level) cover common needs, the library exposes a clean extension point for domain-specific formatting. This guide walks through implementing custom formatter flags using the three core components found in the gabime/spdlog source code.
Understanding the Custom Flag Architecture
The extensibility mechanism spans three interconnected locations in the codebase:
custom_flag_formatter– Abstract base class defined ininclude/spdlog/pattern_formatter.h(lines 56-63) that adds aclone()requirement to the internaldetails::flag_formatterpattern_formatter::add_flag<T>()– Templated registration method ininclude/spdlog/pattern_formatter.h(lines 84-88) that stores custom handlers in thecustom_handlers_mappattern_formatter::handle_flag_()– Compile-time lookup insrc/pattern_formatter-inl.h(lines 21-31) that instantiates custom formatters when pattern strings are parsed
When a pattern containing your custom flag is compiled, spdlog clones the registered formatter via clone() and inserts it into the runtime formatters_ vector. This design ensures thread-safe copying when loggers duplicate formatters.
Step 1: Create Your Custom Flag Formatter
Inherit from spdlog::custom_flag_formatter and implement two pure-virtual methods:
// my_flag.hpp
#pragma once
#include <spdlog/custom_flag_formatter.h>
#include <spdlog/details/log_msg.h>
#include <spdlog/common.h>
#include <spdlog/fmt/fmt.h>
class my_flag : public spdlog::custom_flag_formatter {
public:
explicit my_flag(std::string txt) : text_(std::move(txt)) {}
// Core formatting logic – called for every log record
void format(const spdlog::details::log_msg&, const std::tm& tm,
spdlog::memory_buf_t& dest) override {
// Example: 12-hour clock hour + custom text
std::string out = spdlog::fmt_lib::format(
"{:02d} {}", tm.tm_hour % 12, text_);
dest.append(out.data(), out.data() + out.size());
}
// Required: enables formatter cloning when pattern_formatter is copied
std::unique_ptr<spdlog::custom_flag_formatter> clone() const override {
return spdlog::details::make_unique<my_flag>(text_);
}
private:
std::string text_;
};
The format() method receives:
log_msg– The complete log record metadatatm– Broken-down time structure (reused across formatters for performance)dest– Output buffer to append formatted text
The clone() method must return a new instance with identical configuration. The base class provides padinfo_ member for accessing padding specifications (width, alignment, truncation) if your flag supports modifiers like %10x or %-5x.
Step 2: Register Your Flag with the Pattern Formatter
Use add_flag<T>() to bind a character to your formatter type. Constructor arguments forward directly to your class:
#include "my_flag.hpp"
#include <spdlog/spdlog.h>
#include <spdlog/pattern_formatter.h>
int main() {
auto fmt = std::make_shared<spdlog::pattern_formatter>();
// Register 'x' flag with constructor argument "CUSTOM"
fmt->add_flag<my_flag>('x', "CUSTOM");
// Use padding: %5x means width=5, default right-aligned
fmt->set_pattern("[%n] [%5x] %v");
auto logger = spdlog::stdout_logger_mt("example");
logger->set_formatter(fmt);
logger->info("hello world");
}
Expected output (assuming 3 AM execution):
[example] [ 03 CUSTOM] hello world
The add_flag<T>() method returns *this, enabling chained registration of multiple flags:
fmt->add_flag<my_flag>('x', "FIRST")
.add_flag<another_flag>('y', 42, "SECOND")
.set_pattern("[%x] [%10y] %v");
Step 3: Handle Advanced Scenarios
Exception Safety in Custom Flags
Custom flags may throw, and spdlog propagates these exceptions. The test suite in tests/test_pattern_formatter.cpp (lines 67-87) demonstrates this pattern:
class custom_test_flag : public spdlog::custom_flag_formatter {
public:
explicit custom_test_flag(std::string txt) : payload_(std::move(txt)) {}
void format(const spdlog::details::log_msg&, const std::tm&,
spdlog::memory_buf_t& dest) override {
if (payload_ == "throw_me")
throw spdlog::spdlog_ex("custom_flag_exception_test");
dest.append(payload_.data(), payload_.data() + payload_.size());
}
std::unique_ptr<spdlog::custom_flag_formatter> clone() const override {
return spdlog::details::make_unique<custom_test_flag>(payload_);
}
private:
std::string payload_;
};
Registered and tested as:
auto fmt = std::make_shared<spdlog::pattern_formatter>();
fmt->add_flag<custom_test_flag>('t', "throw_me")
.add_flag<custom_test_flag>('u', "safe_value")
.set_pattern("[%t] [%u] %v");
Accessing Padding Information
The custom_flag_formatter base class exposes padinfo_ for implementing width-aware formatting:
void format(const spdlog::details::log_msg&, const std::tm&,
spdlog::memory_buf_t& dest) override {
std::string raw = generate_content();
// Apply standard spdlog padding (width, align left/right, truncate)
if (padinfo_.enabled_) {
// Use padinfo_.width_, padinfo_.align_, padinfo_.truncate_
// Or delegate to helper methods in the base class
}
dest.append(raw.data(), raw.data() + raw.size());
}
Key Implementation Files
| File | Purpose | Location |
|---|---|---|
include/spdlog/pattern_formatter.h |
custom_flag_formatter base class and add_flag<T>() API |
include/spdlog/pattern_formatter.h |
src/pattern_formatter-inl.h |
Pattern compilation and flag resolution logic | src/pattern_formatter-inl.h |
tests/test_pattern_formatter.cpp |
Reference implementations and edge cases | tests/test_pattern_formatter.cpp |
Summary
- Derive from
spdlog::custom_flag_formatterto define custom pattern flag behavior - Implement both
format()for output generation andclone()for formatter duplication - Register via
pattern_formatter::add_flag<T>(char, Args&&...)before pattern compilation - Access
padinfo_for built-in padding support andtmfor time-based formatting - Clone properly to ensure thread-safe formatter sharing across logger copies
The custom_handlers_ unordered map in pattern_formatter stores registered flags as std::unique_ptr<custom_flag_formatter>, allowing multiple distinct custom flags per formatter instance with O(1) lookup during pattern compilation.
Frequently Asked Questions
What characters can I use for custom flag names?
Any single ASCII character except those reserved for built-in flags (v, t, n, l, L, a, A, b, B, c, C, Y, D, m, d, H, I, M, S, e, f, F, p, r, R, T, z, Z, E, P, s, X, i, u, o, O, %). Review the complete list in src/pattern_formatter-inl.h to avoid collisions.
Do custom flags support padding specifiers like %10x or %-5x?
Yes. The padinfo_ member inherited from details::flag_formatter contains parsed width, alignment, and truncation settings. Implement padding logic manually in format(), or use spdlog's internal padding helpers for consistent behavior with built-in flags.
Why does my custom flag require a clone() method?
pattern_formatter instances are frequently copied when loggers are cloned or when asynchronous sinks duplicate formatters for thread isolation. The clone() method ensures each copy receives an independent instance with identical configuration. Failure to implement this causes compilation errors since custom_flag_formatter declares clone() as pure virtual.
Can I register the same custom flag class with different characters?
Yes. Call add_flag<T>() multiple times with different characters and constructor arguments. Each registration creates a distinct entry in custom_handlers_. The test suite demonstrates this with custom_test_flag registered as both 't' and 'u' with different payloads.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →