Skip to main content

Basic formatting

spdlog's format strings are fmt's format strings — the same {} mini-language, with the same compile-time-checkable syntax, whether you knew that or not when you wrote your first spdlog::info("{}", x).

Replacement fields

spdlog::info("connected to {}", host); // positional, implicit
spdlog::info("{1} before {0}", "second", "first"); // explicit index: "first before second"
spdlog::info("retry {:>4}/{}", attempt, max_attempts); // right-aligned, width 4
spdlog::info("elapsed: {:.3f}s", elapsed_seconds); // fixed, 3 decimal places

{} consumes the next argument in order; {N} picks argument N explicitly, useful when the same value appears more than once. The : introduces a format spec — alignment, width, precision, and type presentation, all inherited directly from fmt.

Why not printf or streams

printfiostreamsspdlog/fmt
Type safetyNone — mismatched %d/%s is UBCompile-time via overloadsCompile-time checkable
ExtensibilityNoneoperator<< overloadfmt::formatter<T> specialization
ThroughputFast, but locale/format-string parsing per callSlow — virtual dispatch, locale checksFastest of the three in benchmarks
Translation-friendliness{0}-style reordering not portableAwkward — order is call order{1} {0} reordering built in

Formatting your own types

Two ways to make a custom type loggable. Preferred: specialize fmt::formatter<T>.

point_formatter.hpp
struct Point { int x, y; };

template <>
struct fmt::formatter<Point> : fmt::formatter<std::string> {
auto format(const Point& p, format_context& ctx) const {
return fmt::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};

// spdlog::info("cursor at {}", Point{3, 7}); // -> "cursor at (3, 7)"

If the type already has operator<<(std::ostream&, const T&), #include "spdlog/fmt/ostr.h" makes it loggable without writing a formatter — slower than a native specialization, but zero extra code for types you don't own.

Argument evaluation

Arguments to spdlog::info(...) and friends are evaluated whether or not the level is enabled — the level check happens, but by the time it does, the arguments have often already been computed as part of building the call.

An expensive argument still costs you at a disabled level
spdlog::debug("state: {}", expensive_serialize(state)); // expensive_serialize() runs
// even if debug is disabled

If an argument is genuinely expensive to compute, guard it explicitly or use the SPDLOG_* macros with a low SPDLOG_ACTIVE_LEVEL — see Compile-time log level for the version that removes both the call and the argument evaluation entirely.

Escaping braces

Literal { and } in a message need doubling: {{ and }}.

A user-supplied string used as a format string is a bug
spdlog::info(user_input); // wrong: user_input is parsed as a format string
spdlog::info("{}", user_input); // right: user_input is just an argument

The first form lets a user-controlled string containing {} crash or misbehave your logging. Always pass untrusted text as an argument, never as the format string itself.

See also