How to Create User-Defined Formatters with fmt::formatter in spdlog
You can add custom formatting to spdlog by specializing fmt::formatter<T> for your types to control how they appear in log messages, or by inheriting from spdlog::formatter to customize the entire log line structure including timestamps and severity levels.
spdlog is a header-only C++ logging library built on top of the {fmt} formatting engine. While spdlog provides powerful pattern formatters out of the box, creating user-defined formatters with fmt::formatter in spdlog allows you to seamlessly log custom objects and control output layout with compile-time type safety.
Specializing fmt::formatter for Custom Types
The most efficient way to format your own structs or classes in spdlog is to extend the fmt library itself. Because spdlog delegates all argument formatting to fmt::format, you simply specialize fmt::formatter for your type.
Implementation Steps
First, define your custom type:
struct Person {
std::string name;
int age;
};
Next, specialize fmt::formatter for that type. The specialization requires two methods: parse() to handle format specifications, and format() to produce the output:
template <> struct fmt::formatter<Person> {
// Parse any format spec (e.g. "{}" or "{:upper}")
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
// format() receives the Person and an output iterator.
template <typename FormatContext>
auto format(const Person& p, FormatContext& ctx) const {
// Use fmt to produce the string; you can honour format specs here.
return fmt::format_to(ctx.out(), "{} ({} yrs)", p.name, p.age);
}
};
Now use Person directly in spdlog calls:
spdlog::info("User: {}", Person{"Alice", 30});
When spdlog formats the message, it forwards arguments to fmt. Since fmt::formatter<Person> is defined, fmt::format_to inside spdlog::pattern_formatter (see include/spdlog/pattern_formatter.h at line 65) automatically invokes your specialization, yielding Alice (30 yrs).
Implementing a Custom spdlog::Formatter
When you need to control all parts of the log line (timestamp format, level placement, thread ID, etc.), you must implement a class derived from spdlog::formatter. This interface is defined in include/spdlog/formatter.h at line 23.
Required Virtual Interface
Your custom formatter must implement two pure virtual members:
std::unique_ptr<formatter> clone() const– Creates a copy of the formatter when sinks are duplicated.void format(const spdlog::details::log_msg&, spdlog::memory_buf_t&)– Writes the formatted output into the supplied buffer.
The buffer type spdlog::memory_buf_t is an alias for fmt::basic_memory_buffer<char>, allowing direct use of fmt::format_to.
Complete Implementation Example
#include <spdlog/formatter.h>
#include <spdlog/details/log_msg.h>
#include <spdlog/pattern_formatter.h> // optional, for reuse of existing parts
#include <fmt/chrono.h> // fmt helpers for time
class my_formatter : public spdlog::formatter {
public:
// Required clone for sink duplication
std::unique_ptr<formatter> clone() const override {
return std::make_unique<my_formatter>(*this);
}
// Core formatting routine
void format(const spdlog::details::log_msg& msg,
spdlog::memory_buf_t& dest) override {
// Example: "[%Y-%m-%d %H:%M:%S.%e] [level] message"
using clock = std::chrono::system_clock;
std::time_t t = clock::to_time_t(msg.time);
std::tm tm = *std::localtime(&t);
// Use fmt to build the prefix
fmt::format_to(std::back_inserter(dest),
"[{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}] ",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
std::chrono::duration_cast<std::chrono::milliseconds>(msg.time.time_since_epoch()).count() % 1000);
// Level name
fmt::format_to(std::back_inserter(dest), "[{}] ", spdlog::level::to_string_view(msg.level));
// The original message payload (already formatted by fmt)
fmt::format_to(std::back_inserter(dest), "{}", fmt::string_view(msg.payload.data(), msg.payload.size()));
}
};
Installing Your Formatter
Install the formatter globally to affect all sinks:
spdlog::set_formatter(std::make_unique<my_formatter>());
This function is implemented in include/spdlog/spdlog-inl.h at line 23.
Alternatively, install per-sink by calling set_formatter on the sink object. Each sink inherits from spdlog::sinks::base_sink, which stores its own std::unique_ptr<formatter> (see include/spdlog/sinks/base_sink.h at line 22):
sink->set_formatter(std::make_unique<my_formatter>());
Key Source Files
The spdlog formatting hierarchy relies on these specific implementation files:
include/spdlog/formatter.h(line 23): Defines the abstractspdlog::formatterbase class.include/spdlog/pattern_formatter.h(line 65): Contains the defaultspdlog::pattern_formatterimplementation.include/spdlog/sinks/base_sink.h(line 22): Thebase_sinkclass template that holds per-sink formatters.include/spdlog/spdlog-inl.h(line 23): Implementsspdlog::set_formatter()for global configuration.include/spdlog/details/os.h: Indirectly definesspdlog::memory_buf_tvia the fmt library type alias.
Summary
- Specialize
fmt::formatter<T>when you want spdlog to automatically format your custom types in log statements; this leverages the fmt library directly and requires no additional spdlog code. - Inherit from
spdlog::formatterwhen you need complete control over the log line layout, including timestamps, log levels, and message structure. - The
format()method receives aspdlog::details::log_msgcontaining metadata (time, level, thread info) and aspdlog::memory_buf_tbuffer for output. - Install custom formatters globally via
spdlog::set_formatter()or per-sink viasink->set_formatter(). - All formatting ultimately delegates to the fmt library, so you can use any fmt API (
fmt::format_to,fmt::format, etc.) within your implementations.
Frequently Asked Questions
What is the difference between fmt::formatter and spdlog::formatter?
A specialization of fmt::formatter<T> tells the fmt library how to convert a specific type T into text during the formatting of a log message argument. spdlog::formatter is an abstract base class that controls the entire log line output, including metadata like timestamps and severity levels. Use fmt::formatter for custom types; use spdlog::formatter for custom line layouts.
How do I handle custom format specifiers in my fmt::formatter specialization?
The parse() method in your fmt::formatter specialization receives a format_parse_context containing any text between the : and the } in a format string (e.g., {:upper}). Parse this text to store configuration flags, then return an iterator pointing past the closing }. The format() method can then access these settings to alter output behavior.
Can I use a custom spdlog::formatter and custom fmt::formatters together?
Yes. A custom spdlog::formatter implementation typically calls fmt::format_to internally to render the message payload. If the payload contains user-defined types with fmt::formatter specializations, fmt will automatically invoke them. This allows you to customize the line structure while still benefiting from type-specific formatters for individual arguments.
Why does my custom spdlog::formatter need a clone() method?
The clone() method creates a copy of the formatter instance. spdlog calls this when it needs to duplicate sinks (for example, when creating thread-local copies or copying configuration). Because formatters are stored as std::unique_ptr<formatter>, the clone pattern enables proper deep copying of polymorphic formatter objects.
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 →