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_formatterininclude/spdlog/pattern_formatter.hto create new pattern flags - Implement
clone()to enable deep-copying when formatters are duplicated - Implement
format()to write output to thememory_buf_tdestination 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%5xor%-10xin pattern strings - Reference
tests/test_pattern_formatter.cppfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →