spdlog是c++最稳妥的轻量日志库,5行代码即可实现线程安全、异步写入的文件日志;需注意编译期级别控制、显式shutdown、避免共享格式器等关键实践。

用 spdlog 快速接入日志,5 行代码搞定基础输出
不用自己写线程安全的文件写入、不用手动管理缓冲区,spdlog 是目前 C++ 项目里最稳妥的轻量选择。它编译快、头文件少、默认就支持多线程和异步写入。
实操建议:
- 用
cmake的find_package(spdlog REQUIRED)比直接拉源码更稳,避免宏定义冲突 - 初始化必须在
main()开头调用spdlog::set_default_logger(),否则后续spdlog::info()会静默失败 - 别用
spdlog::stdout_logger_mt()做生产环境主日志——它不落盘,进程退出时未 flush 的日志就丢了
示例(最小可行):
#include <spdlog/spdlog.h>
int main() {
spdlog::set_default_logger(spdlog::basic_logger_mt("app", "app.log"));
spdlog::info("startup ok");
return 0;
}
日志级别控制:为什么 SPDLOG_COMPILED_LEVEL 要设在编译期
运行时用 logger->set_level() 只能过滤输出,但字符串拼接、参数求值、格式化这些开销依然发生。真正零成本裁剪得靠编译期宏。
立即学习“C++免费学习笔记(深入)”;
常见错误现象:
- 加了
spdlog::debug("slow_func() = {}", slow_func());,但调试关了还卡顿——因为slow_func()仍被执行 - 用
#define SPDLOG_COMPILED_LEVEL SPDLOG_LEVEL_INFO却没生效——宏必须在包含spdlog.h前定义
正确姿势:
- 在
CMakeLists.txt里统一加add_compile_definitions(SPDLOG_COMPILED_LEVEL=SPDLOG_LEVEL_INFO) - 调试阶段临时开
DEBUG,只改这一处,所有日志点自动生效或剔除
异步日志崩了没报错?检查 spdlog::init_thread_pool() 和生命周期
异步模式下,日志实际由后台线程池处理。如果主线程结束太快,线程池还没 flush 完就析构,日志就丢——而且不抛异常、不打 warning。
使用场景:
- 命令行工具、单元测试这类短生命周期程序,异步日志风险极高
- 服务类程序可放心用,但必须确保
spdlog::shutdown()在main()结束前被调用
关键点:
- 调用
spdlog::daily_logger_mt()等工厂函数前,先执行spdlog::init_thread_pool(8192, 1)(队列大小 ≥ 日志峰值,线程数 ≥ 1) - 不要依赖 RAII 自动析构——
spdlog::shutdown()必须显式调用,且不能晚于任何 logger 实例销毁
自定义格式器踩坑:别直接改 pattern_formatter 的 set_pattern()
想加 trace_id 或进程 ID?直接调 set_pattern("%v") 看似简单,但会导致所有 logger 共享同一份格式器实例,多线程下格式串可能被覆盖。
性能影响:
- 每次
set_pattern()都重建正则解析树,高频调用会拖慢日志吞吐 - 格式器对象不是线程安全的,多个 logger 复用同一个实例会触发未定义行为
正确做法:
- 每个 logger 单独 new 一个
spdlog::pattern_formatter,再 set 给它 - 需要动态字段(如 trace_id),优先用
spdlog::sinks::dist_sink+ 自定义 sink,而不是硬塞进 pattern - 实在要改 pattern,用
spdlog::set_pattern()(全局)或构造时传参,别运行时反复 set
复杂点在于:日志上下文(比如当前请求的 trace_id)没法靠格式器自动注入,得配合 thread_local 变量 + 自定义 sink 才能干净解耦。这点容易被忽略,一上来就想在 pattern 里写 %x,结果发现根本没地方塞值。










