How to Use spdlog with Different Output Formats (e.g., JSON)

To output logs in JSON or other formats with spdlog, implement the spdlog::formatter interface and attach it to a logger via set_formatter().

The gabime/spdlog library provides a flexible formatting system centered on the spdlog::formatter abstraction defined in include/spdlog/formatter.h. While the default spdlog::pattern_formatter produces human-readable text based on pattern strings, you can create custom formatters to emit structured formats like JSON, CSV, or XML. This guide demonstrates how to implement custom formatters using the bundled fmt library or optional nlohmann/json integration.

Understanding the spdlog Formatter Architecture

The formatter architecture in spdlog revolves around a base class that requires two pure virtual methods. According to include/spdlog/formatter.h, every formatter must implement:

  1. format(const spdlog::details::log_msg& msg, fmt::memory_buffer& dest) — Writes the formatted representation into the destination buffer.
  2. clone() — Returns a std::unique_ptr<spdlog::formatter> copy required for spdlog's copy-on-write semantics.

When you call set_formatter() on a logger, every sink attached to that logger uses your custom formatter to process log messages before output. The library bundles fmt for formatting operations (see src/bundled_fmtlib_format.cpp), which you can leverage inside your formatter implementation.

Creating a Custom JSON Formatter

To emit JSON logs, subclass spdlog::formatter and implement the required interface methods. Below are two approaches: one using only the bundled fmt library for lightweight deployment, and another using nlohmann/json for robust JSON handling (as demonstrated in include/spdlog/sinks/loki_sink.h).

Method 1: Minimal JSON with fmt Only

This approach manually constructs JSON strings using fmt::format_to, avoiding external dependencies. It is suitable for simple key-value logging where you control the message content.

#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/formatter.h>
#include <fmt/chrono.h>
#include <fmt/format.h>

class json_formatter : public spdlog::formatter {
public:
    void format(const spdlog::details::log_msg& msg,
                fmt::memory_buffer& dest) override {
        // Convert timestamp to ISO‑8601 string
        auto tp = std::chrono::system_clock::time_point(msg.time);
        std::string ts = fmt::format("{:%FT%TZ}", tp);

        // Build a tiny JSON object manually
        fmt::format_to(std::back_inserter(dest),
                       R"({{"timestamp":"{}","level":"{}","msg":"{}"}})",
                       ts,
                       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>();
    }
};

int main() {
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.json");
    auto logger    = std::make_shared<spdlog::logger>("json_logger", file_sink);

    logger->set_formatter(std::make_unique<json_formatter>());
    spdlog::set_default_logger(logger);

    spdlog::info("server started on port {}", 8080);
    spdlog::error("failed to open config file {}", "config.yaml");
}

Output (app.json):

{"timestamp":"2026-07-15T12:34:56Z","level":"info","msg":"server started on port 8080"}
{"timestamp":"2026-07-15T12:35:01Z","level":"error","msg":"failed to open config file config.yaml"}

Method 2: Structured JSON with nlohmann/json

For complex JSON objects with proper escaping and nested structures, use the nlohmann::json library. The spdlog repository already uses this pattern in the Loki sink implementation (include/spdlog/sinks/loki_sink.h).

#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/formatter.h>
#include <nlohmann/json.hpp>

class json_nlohmann_formatter : public spdlog::formatter {
public:
    void format(const spdlog::details::log_msg& msg,
                fmt::memory_buffer& dest) override {
        nlohmann::json j;
        j["timestamp"] = fmt::format("{:%FT%TZ}", std::chrono::system_clock::time_point(msg.time));
        j["level"]     = spdlog::level::to_string_view(msg.level);
        j["msg"]       = fmt::string_view(msg.payload.data(), msg.payload.size());

        // Serialize without pretty‑printing (compact for logs)
        std::string s = j.dump();
        fmt::format_to(std::back_inserter(dest), "{}", s);
    }

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

int main() {
    auto console = spdlog::stdout_color_mt("console");
    console->set_formatter(std::make_unique<json_nlohmann_formatter>());

    spdlog::set_default_logger(console);
    spdlog::warn("disk usage at {}%", 92);
}

Output:

{"timestamp":"2026-07-15T12:36:12Z","level":"warning","msg":"disk usage at 92%"}

Swapping Formatters at Runtime

spdlog allows dynamic format switching on existing loggers without recreation. You can alternate between human-readable patterns and machine-readable JSON to support different operational phases.

auto logger = spdlog::stdout_color_mt("dynamic");

// Human‑readable pattern (default)
logger->info("starting in plain mode");

// Switch to JSON for structured logging phase
logger->set_formatter(std::make_unique<json_formatter>());
logger->info("phase", "json");

// Switch back to pattern formatter
logger->set_formatter(std::make_unique<spdlog::pattern_formatter>("%^%L%$ %v"));
logger->info("back to plain text");

This flexibility enables scenarios where you log to a file in JSON for machine consumption while keeping console output human-readable during development.

Summary

  • Implement the spdlog::formatter interface from include/spdlog/formatter.h to define custom output formats
  • Override format() to serialize log messages and clone() to support copy-on-write semantics
  • Register custom formatters via set_formatter() on any logger instance to apply them to all attached sinks
  • Use fmt::format_to for lightweight JSON construction or integrate nlohmann/json for robust escaping and object handling
  • Switch formatters at runtime to adapt output formats for different environments without restarting the application

Frequently Asked Questions

Can I use different formats for different sinks on the same logger?

No. In spdlog, formatters are attached to loggers, not individual sinks. If you need different formats for different outputs (e.g., JSON to file and plain text to console), create separate logger instances with distinct formatters, or implement a custom sink that handles its own formatting. Each sink attached to a logger receives the same formatted output.

Does spdlog support JSON formatting natively without custom code?

No. spdlog does not ship with a built-in JSON formatter. You must implement the spdlog::formatter interface as shown above. While you could use the pattern formatter with JSON-like patterns, this approach is not recommended for production due to escaping issues and lack of structured data support.

How do I handle JSON escaping in custom formatters?

When using the fmt-only approach, you must manually escape quotes, backslashes, and newlines in message payloads. For production use, leverage the nlohmann/json library (referenced in include/spdlog/sinks/loki_sink.h), which automatically escapes strings when calling dump(). This prevents malformed JSON from special characters in log messages.

Is the custom formatter thread-safe?

Yes. spdlog handles thread safety at the sink level using mutexes, not at the formatter level. The format() method is called within the sink's locking mechanism. However, your formatter implementation should not maintain mutable state between calls unless properly synchronized. The clone() method ensures spdlog can create formatter copies when needed for thread isolation.

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 →