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:

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::formatter when you need complete control over the log line layout, including timestamps, log levels, and message structure.
  • The format() method receives a spdlog::details::log_msg containing metadata (time, level, thread info) and a spdlog::memory_buf_t buffer for output.
  • Install custom formatters globally via spdlog::set_formatter() or per-sink via sink->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:

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 →