How to Create Custom Formatters for spdlog: A Complete Implementation Guide

To create a custom formatter for spdlog, inherit from spdlog::formatter and implement the pure virtual format() and clone() methods, then attach the instance to a logger via set_formatter().

spdlog is a header-only C++ logging library renowned for its speed and extensibility. While it ships with a powerful pattern-based formatter, many applications require specialized output formats such as JSON or structured logging. This guide explains how to create custom formatters for spdlog by leveraging the library's architecture defined in include/spdlog/formatter.h and include/spdlog/pattern_formatter.h.

Understanding the spdlog Formatter Architecture

The formatting pipeline centers on the abstract base class spdlog::formatter defined in include/spdlog/formatter.h. Every logger maintains a std::unique_ptr<formatter> that transforms details::log_msg objects into text buffers.

To implement a custom formatter, you must override two pure virtual methods:

  • void format(const details::log_msg &msg, memory_buf_t &dest): Converts the incoming log message into text and appends it to the destination buffer.
  • std::unique_ptr<formatter> clone() const: Returns a deep copy of the formatter instance. spdlog invokes this when duplicating loggers to ensure thread-safe, independent formatter states.

The details::log_msg structure passed to format() contains pre-processed metadata including timestamps, log levels, thread IDs, source file locations, and the message payload. Because spdlog may clone formatters across threads, your implementation must remain stateless or store mutable state only within the instance itself.

Creating a Full Custom Formatter

When you need complete control over output serialization—such as emitting JSON or binary formats—inherit directly from spdlog::formatter. The following example implements a JSON formatter that outputs structured log records.

#include <spdlog/spdlog.h>
#include <spdlog/details/log_msg.h>
#include <spdlog/fmt/fmt.h>

class json_formatter : public spdlog::formatter {
public:
    void format(const spdlog::details::log_msg &msg,
                spdlog::memory_buf_t &dest) override {
        fmt::format_to(
            dest,
            R"({{"ts":"{:%FT%TZ}","lvl":"{}","msg":"{}"}}\n)",
            msg.time,
            spdlog::level::to_string_view(msg.level),
            fmt::string_view{msg.payload.data(), msg.payload.size()});
    }

    std::unique_ptr<spdlog::formatter> clone() const override {
        return std::make_unique<json_formatter>(*this);
    }
};

This formatter accesses msg.time (a std::chrono::system_clock::time_point) and uses fmt library syntax to produce ISO-8601 timestamps. The clone() method performs a deep copy via std::make_unique, satisfying spdlog's requirements for logger duplication.

Extending the Pattern Formatter with Custom Flags

If you only need to add custom placeholders to the existing pattern syntax, subclass spdlog::custom_flag_formatter from include/spdlog/pattern_formatter.h instead of rewriting the entire formatter. This approach allows you to register new % specifiers while retaining the default pattern formatting logic.

Implement format_flag() to append your custom data, and override clone() to return a copy:

#include <spdlog/pattern_formatter.h>
#include <unistd.h>

class pid_flag : public spdlog::custom_flag_formatter {
public:
    void format_flag(const spdlog::details::log_msg&, 
                     fmt::format_context::iterator out) override {
        fmt::format_to(out, "{}", static_cast<int>(::getpid()));
    }

    std::unique_ptr<spdlog::custom_flag_formatter> clone() const override {
        return std::make_unique<pid_flag>(*this);
    }
};

Register the flag with a pattern_formatter instance before attaching it to a logger:

auto formatter = std::make_unique<spdlog::pattern_formatter>("%+ [%P]");
formatter->add_flag<pid_flag>('P', "process id");
logger->set_formatter(std::move(formatter));

After registration, the %P specifier outputs the current process ID alongside standard log data.

Attaching Custom Formatters to Loggers

Once instantiated, attach your custom formatter using the set_formatter() method available on all spdlog logger instances. This operation replaces any existing formatter immediately.

auto logger = spdlog::stdout_color_mt("my_logger");
logger->set_formatter(std::make_unique<json_formatter>());

logger->info("Application started");

You can switch formatters at runtime to accommodate different logging modes—such as switching from human-readable text during development to JSON in production:

// Development mode
logger->set_pattern("[%H:%M:%S] [%l] %v");

// Production mode
logger->set_formatter(std::make_unique<json_formatter>());

Summary

  • Inherit from spdlog::formatter defined in include/spdlog/formatter.h to create fully custom output formats.
  • Implement format() to serialize details::log_msg into the provided memory_buf_t buffer using the fmt library.
  • Implement clone() to return std::make_unique<YourFormatter>(*this) for safe logger duplication.
  • Use spdlog::custom_flag_formatter from include/spdlog/pattern_formatter.h to add new % placeholders without rewriting the entire formatting engine.
  • Attach formatters via logger->set_formatter() and swap them at runtime as needed.

Frequently Asked Questions

What is the difference between spdlog::formatter and spdlog::pattern_formatter?

spdlog::formatter is the abstract base class that defines the interface for all formatters. spdlog::pattern_formatter is a concrete implementation that parses format strings like "%+ [%H:%M:%S]" and converts them into log output. You inherit from formatter for complete control over serialization, or extend pattern_formatter via custom_flag_formatter to add specific placeholders.

Why does my custom formatter need a clone() method?

spdlog uses the clone() method to duplicate formatters when copying logger instances or creating asynchronous sinks. Without a proper implementation, loggers sharing formatter state could cause data races or undefined behavior. Always return a deep copy using std::make_unique<YourClass>(*this).

Can I modify formatter state after attaching it to a logger?

While possible, modifying formatter state after attachment requires synchronization if multiple threads use the same logger. spdlog formatters are designed to be stateless or immutable after creation. Store mutable configuration data externally and pass it to the formatter constructor rather than modifying the formatter instance post-attachment.

How do I access the original format string in a custom flag formatter?

The custom_flag_formatter base class does not provide the full pattern string context. If you need to parse complex interdependencies between flags, implement a complete custom formatter inheriting from spdlog::formatter instead of using the flag extension mechanism.

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 →