Skip to main content

Design philosophy

Three decisions explain almost every API in spdlog: formatting is delegated to fmt, sinks are the one extension point, and work happens off the caller's thread only if you explicitly ask for it. Once those three land, the rest of the API โ€” loggers, the registry, patterns, async โ€” reads as consequences rather than separate features.

Logger โ†’ sinks fan-outโ€‹

Every log call follows the same path:

The logger's job stops at "should this message exist at all" โ€” a single level check. Everything after that (formatting, writing, flushing) is delegated to each sink independently, which is why one logger can quietly write plain text to a file and colored text to a console at the same time.

Built on fmtโ€‹

spdlog's format strings are fmt's format strings. That buys two things printf and iostream don't offer together: type safety (the compiler โ€” or fmt itself โ€” catches a mismatched {} argument) and speed (fmt's formatting is consistently faster than both alternatives in benchmarks, because it avoids the locale and virtual-dispatch overhead streams carry and the string-scanning overhead printf carries). See Basic formatting for the syntax.

Speed-first defaultsโ€‹

  • The level check happens before arguments are formatted โ€” a disabled spdlog::debug(...) call costs one comparison, not a string build.
  • The common logging path avoids heap allocation where the message fits in a small stack buffer.
  • Every factory function comes in an _mt (mutex-protected) and _st (single-threaded, no locking) flavor:
SuffixThread safetyCost
_mtSafe to log from multiple threadsOne mutex lock/unlock per message
_stOnly safe from a single threadNo locking overhead

Default to _mt unless you've profiled and confirmed a logger is genuinely single-threaded โ€” the cost of guessing wrong is a data race, not a slow log line.

Extension pointsโ€‹

Sinks are the officially supported place to add behavior spdlog doesn't ship: a new destination (Custom sinks), or a new pattern flag for the line format (Custom formatters). Everything else in the library โ€” loggers, the registry, the thread pool โ€” is infrastructure that exists to get messages to sinks efficiently, not something you're expected to subclass.

The costโ€‹

None of this is free. Header-only mode means real compile-time cost in large projects (see Installation and integration). The vendored fmt copy is extra code in your binary if you don't opt out of it. And the registry โ€” the global nameโ†’logger map that makes spdlog::get("db") work from anywhere โ€” is convenient specifically because it's global state, with all the shutdown-ordering and testability trade-offs that implies.

Global state is a design choice, not an accident

spdlog could have required you to pass a logger reference everywhere. It didn't, because most applications want "log from anywhere without threading a reference through every function" more than they want to avoid a global. See The registry for what that trade actually costs you.

See alsoโ€‹