之前在做 C++ 项目日志模块时,反复纠结于是用 printf 打点、OutputDebugString 输出,还是自己封装一个文件日志类。前两者功能太弱,自研的轮转、分级、线程安全都要从零实现,费时费力还容易埋坑。后来换上了 spdlog,整个日志模块的代码量从上千行降到几十行,而且性能足够好、功能也完整。这篇文章就把 spdlog 的完整使用思路整理出来,包含基础 API、关键配置、完整示例代码,以及 MFC 项目里的集成过程,新手可以直接照着落地,有 C++ 基础的开发者也能快速对照排查问题。
1. 背景与核心概念
1.1 spdlog 是什么
spdlog 是一个开源的 C++ 日志库,由 Gabi Melman 开发并托管在 GitHub 上,项目仓库为gabime/spdlog。它最大的特点是头文件为主、性能优秀、接口简洁,在 C++ 社区中非常流行。
用一句话概括:spdlog 是 C++ 世界里开箱即用的日志方案,提供格式化的日志输出、多级日志级别、多种输出目标(控制台、文件、调试器、网络等),并且内部处理了线程安全、日志轮转、异步写入等繁琐问题。
与自研日志模块和传统日志库相比,spdlog 的核心优势如下:
- 快速:日志写入通过格式化与批量输出优化,性能高于一般自己写的
fprintf方案。 - 零额外依赖:默认实现不依赖第三方库,使用 C++11 及以上特性。
- 功能完整:支持日志级别、格式 pattern、轮转文件、按大小或时间分割、异步日志、异常回调。
- 接入简单:既可以源码编译,也可以直接引入头文件使用。
- 社区活跃:GitHub 上 star 数量高,issue 更新快,版本迭代稳定。
1.2 spdlog 解决什么问题
在实际项目开发中,日志至少承担这几个职责:
- 问题排查:程序崩溃、逻辑异常时,靠日志还原现场。
- 运行监控:记录请求耗时、接口状态、资源变化。
- 业务审计:保存关键操作记录,方便追溯。
- 开发调试:替代临时 printf,支持分级输出,方便动态开关。
但自研日志往往要面对以下问题:
- 多个线程同时写文件,如果不加锁会出现内容交叉。
- 日志文件越来越大,没有轮转机制。
- 日志格式五花八门,很难统一解析。
- 异步落盘与程序退出之间的数据丢失。
spdlog 已经把这些问题全部处理好了,我们要做的只是配置和使用,这大大降低了日志模块的开发成本。
1.3 常见应用场景
spdlog 适合以下几种场景:
- C++ 服务端程序:Linux/Windows 下的后台服务,需要稳定的文件日志。
- 客户端软件:比如基于 MFC、Qt 的桌面程序,需要同时输出文件和调试窗口日志。
- 工具脚本/中间件:C++ 编写的命令行工具、数据中间件,需要记录运行状态。
- 游戏/嵌入式应用:对性能要求较高的场景,spdlog 的异步模式很适合。
2. 环境准备与版本说明
2.1 spdlog 下载与获取
下载 spdlog 有三种常见方式,读者可以根据项目情况选择。
方式一:GitHub 获取源码
直接访问 GitHub 仓库https://github.com/gabime/spdlog,在 Releases 页面下载最新稳定版的源码包,例如spdlog-x.x.x.zip。这种方式最直观,也方便查看源码和示例。
方式二:vcpkg/conan 包管理
如果项目本身使用 vcpkg,可以直接执行:
vcpkg install spdlog使用 conan 的话,在conanfile.txt中声明:
[requires] spdlog/1.14.1 [generators] cmake需要说明的是,具体版本号请以你执行命令时 vcpkg/conan 仓库中的最新版本为准。
方式三:系统包管理器
Ubuntu/Debian 下可以尝试:
sudo apt-get install libspdlog-dev但系统仓库中的版本可能偏旧,如果对版本有要求,更推荐前两种方式。
2.2 编译与集成方式
spdlog 支持两种集成方式。
仅头文件模式(Header-only)
spdlog 默认是 header-only 的,直接把include/目录加入头文件路径即可,不需要单独编译库文件。这是最快的接入方式,适合中小型项目。
编译成静态库/动态库
如果项目较大,或者希望加快编译速度,可以定义SPDLOG_COMPILED_LIB宏并编译 spdlog 库。CMake 方式如下:
cmake_minimum_required(VERSION 3.10) project(SpdlogDemo) set(CMAKE_CXX_STANDARD 17) add_executable(demo main.cpp) # 方式一:header-only,直接把 include 目录引进来 target_include_directories(demo PRIVATE "path/to/spdlog/include") # 方式二:使用 spdlog 提供的 CMake 目标 # add_subdirectory(path/to/spdlog) # target_link_libraries(demo PRIVATE spdlog::spdlog)建议使用方式二,通过add_subdirectory引入 spdlog,它会自动处理头文件路径和编译选项。如果你用 vcpkg,CMake 中通过find_package(spdlog REQUIRED)也可以。
2.3 版本与编译器说明
不同版本的 spdlog 对 C++ 标准要求略有不同。老版本通常要求 C++11,新版本对 C++17 支持更好。本文示例基于常见的 1.x 版本写法,重点演示用法,具体 API 名称在不同版本间基本稳定。如果遇到编译错误,先检查版本对应文档,再检查编译标准是否设置正确。
示例环境大致如下:
- 操作系统:Windows 10/11 或 Ubuntu 20.04+。
- 编译器:MSVC 2019/2022,或 GCC 9+。
- CMake:3.14 及以上。
- C++ 标准:C++17(建议)。
如果项目中使用了 MFC,请确保 spdlog 的头文件路径与 MFC 头文件路径不冲突,这一点后面会专门说明。
3. 核心用法拆解
3.1 核心对象:logger、sink、formatter
spdlog 中最重要的三个概念是 logger、sink、formatter。
- logger(日志器):对外暴露日志接口的对象,比如
logger->info("...")。一个 logger 内部可以有多个 sink。 - sink(输出目标):日志实际写到哪里,比如控制台、文件、MSVC 调试窗口。每种 sink 负责一种输出渠道。
- formatter(格式化器):决定日志行的格式,比如是否包含时间戳、线程 id、日志级别。
理解三者的关系后,就可以明白 spdlog 为什么灵活:logger 接收日志消息,经过 formatter 格式化,再交给一个或多个 sink 输出。这样我们可以让同一条日志同时输出到控制台和文件,而不用写两遍代码。
下面是一个典型的多 sink 示例:
#include "spdlog/spdlog.h" #include "spdlog/sinks/stdout_color_sinks.h" #include "spdlog/sinks/basic_file_sink.h" int main() { // 创建两个 sink:一个彩色控制台,一个普通文件 auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/multi.log", true); // 创建 logger,并把两个 sink 都挂上去 spdlog::logger my_logger("multi", {console_sink, file_sink}); my_logger.set_level(spdlog::level::info); my_logger.info("这条日志会同时写入控制台和文件"); my_logger.warn("警告信息也会同时输出"); return 0; }注意stdout_color_sink_mt中的_mt表示 multi-thread,即线程安全版本。spdlog 提供了_mt和_st两种版本,前者适合多线程环境,后者适合单线程环境,性能略高。
3.2 日志级别说明
spdlog 的日志级别从低到高为:
| 级别 | 枚举值 | 说明 |
|---|---|---|
| trace | spdlog::level::trace | 最详细,一般用于调试 |
| debug | spdlog::level::debug | 调试信息 |
| info | spdlog::level::info | 普通信息 |
| warn | spdlog::level::warn | 警告 |
| err | spdlog::level::err | 错误 |
| critical | spdlog::level::critical | 严重错误,可能导致程序退出 |
设置 logger 级别后,低于该级别的日志不会被输出。比如设置为info,则trace和debug不会记录,这样可以在发布版本中减少日志写入量。
logger->set_level(spdlog::level::info); logger->trace("这条不会输出"); logger->debug("这条不会输出"); logger->info("这条会输出");3.3 格式化 pattern 详解
在真实项目中,我们不可能只记录一条孤立的文本,通常需要包含时间、线程、日志级别、源文件位置等信息。spdlog 通过 pattern 语法控制输出格式。
常用 pattern 标志:
| 标志 | 含义 |
|---|---|
%Y-%m-%d | 年-月-日 |
%H:%M:%S | 时:分:秒 |
%e | 毫秒 |
%t | 线程 id |
%l | 日志级别 |
%n | logger 名称 |
%s | 源文件名 |
%# | 源文件行号 |
%! | 函数名 |
%v | 日志内容本身 |
%P | 进程 id |
示例:
#include "spdlog/spdlog.h" int main() { auto logger = spdlog::stdout_color_mt("console"); logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [thread %t] [%s:%#] %v"); logger->info("用户登录成功"); return 0; }输出效果类似:
[2025-01-18 14:22:31.128] [info] [thread 1234] [main.cpp:12] 用户登录成功注意:%s、%#、%!这类源码位置信息需要在编译时启用SPDLOG_ACTIVE_LEVEL宏才能完整记录。后面的最佳实践章节会提到这一点。
3.4 刷新策略与性能考虑
spdlog 默认情况下的刷盘策略是为了平衡性能与实时性。如果你的日志非常重要,希望每条日志都立即写入磁盘,可以设置刷新策略:
// 每条日志都立即刷盘 logger->flush_on(spdlog::level::info); // 或者在程序退出时统一刷新 spdlog::shutdown();在异步模式下,日志先进入队列,由后台线程批量写入文件。这样做的好处是主线程不会因为磁盘 IO 而阻塞,缺点是如果程序崩溃,队列中尚未写入的数据可能会丢失。所以对关键系统,要合理选择异步队列大小和 flush 策略。
4. 完整实战案例
4.1 控制台与文件日志基础案例
先看一个最完整的入门示例。假设我们的项目结构如下:
spdlog_demo/ ├── CMakeLists.txt └── src/ └── main.cppCMakeLists.txt内容:
cmake_minimum_required(VERSION 3.14) project(SpdlogDemo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(spdlog_demo src/main.cpp) # 这里假设你已经下载 spdlog 源码并放在 third_party/spdlog 目录 add_subdirectory(third_party/spdlog) target_link_libraries(spdlog_demo PRIVATE spdlog::spdlog)如果你的环境不想用 CMake,也可以直接把 spdlog 的include目录加入 IDE 头文件包含路径,然后包含头文件即可。
src/main.cpp内容:
#include "spdlog/spdlog.h" #include "spdlog/sinks/basic_file_sink.h" #include "spdlog/sinks/rotating_file_sink.h" int main() { // 1. 初始化一个文件 logger,第二个参数 true 表示追加写 auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/app.log", true); file_logger->set_level(spdlog::level::debug); file_logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] %v"); // 2. 输出不同级别日志 file_logger->debug("这是一条 debug 日志"); file_logger->info("这是一条 info 日志"); file_logger->warn("这是一条 warn 日志"); file_logger->error("这是一条 error 日志,参数:{}", 42); // 3. 使用花括号格式化 file_logger->info("用户名:{},年龄:{}", "Alice", 25); // 4. 程序退出时刷新并释放资源 spdlog::shutdown(); return 0; }运行程序后,logs/app.log文件中会出现类似内容:
[2025-01-18 14:30:01.123] [debug] [4421] 这是一条 debug 日志 [2025-01-18 14:30:01.123] [info] [4421] 这是一条 info 日志 [2025-01-18 14:30:01.124] [warn] [4421] 这是一条 warn 日志 [2025-01-18 14:30:01.124] [error] [4421] 这是一条 error 日志,参数:42 [2025-01-18 14:30:01.124] [info] [4421] 用户名:Alice,年龄:25这段代码里最重要的两个点是:
spdlog::basic_logger_mt创建的 logger 默认是多线程安全的,适合在多个线程中共享使用。{}是 spdlog 的格式化占位符,相当于 C 语言printf里的%d、%s,但类型安全。
4.2 轮转文件日志(按大小分割)
实际项目中,单文件日志会越来越大,既不便于查看,也容易占满磁盘。spdlog 的rotating_logger_mt支持按大小自动轮转。
#include "spdlog/spdlog.h" #include "spdlog/sinks/rotating_file_sink.h" int main() { // 参数说明: // 第一个参数:logger 名称 // 第二个参数:日志文件路径 // 第三个参数:每个日志文件最大字节数(这里 1MB) // 第四个参数:最多保留日志文件个数 auto logger = spdlog::rotating_logger_mt("rotating", "logs/rotate.log", 1024 * 1024, 3); logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v"); for (int i = 0; i < 10000; ++i) { logger->info("这是第 {} 条日志", i); } spdlog::shutdown(); return 0; }当logs/rotate.log超过 1MB 时,spdlog 会自动把它改名为rotate.1.log,并新建一个新的rotate.log。如果已经有rotate.1.log,则继续滚成rotate.2.log。最多保留 3 个文件,超出后最老的会被删除。
这种机制在生产环境非常实用,建议在服务类程序中优先使用轮转文件日志。
4.3 异步日志的配置
如果日志写入量很大,或者在主线程中写日志不能阻塞业务逻辑,可以用异步模式。异步模式下,日志先进入队列,后台线程负责批量写入。
#include "spdlog/spdlog.h" #include "spdlog/async.h" #include "spdlog/sinks/stdout_color_sinks.h" #include "spdlog/sinks/basic_file_sink.h" int main() { // 初始化异步线程池,参数1:队列大小,参数2:后台线程数 spdlog::init_thread_pool(8192, 1); // 创建异步文件 logger auto logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>("async_logger", "logs/async.log", true); logger->set_level(spdlog::level::info); logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] %v"); logger->info("异步日志写入开始"); for (int i = 0; i < 100; ++i) { logger->info("异步日志:{}", i); } // 异步模式下,一定要调用 shutdown 或 drop,否则可能丢失队列中的日志 spdlog::shutdown(); return 0; }需要提醒的是,异步日志在程序崩溃时会存在丢失风险。对于记录崩溃原因的日志,可以考虑单独使用同步模式,确保数据马上落盘。
4.4 MFC 集成 spdlog 的完整案例
接下来看一个更贴近实际工程的案例:在 MFC 项目中使用 spdlog 例子代码。
MFC 项目与普通控制台程序有几个不同点:
- MFC 项目通常使用 Unicode 字符集,即
CString内部是wchar_t。 - 除了写文件,我们还希望日志输出到 Visual Studio 的输出窗口,方便调试。
- 页面卡死或崩溃前,往往需要将诊断信息写入本地文件。
下面给出标准做法。
4.4.1 创建 MFC 项目结构
MfcSpdlogDemo/ ├── MfcSpdlogDemo.h ├── MfcSpdlogDemo.cpp ├── MfcSpdlogDemoDlg.h ├── MfcSpdlogDemoDlg.cpp └── spdlog/ └── include/ ...假设项目已经创建好了 MFC 对话框程序,接下来做日志封装。
4.4.2 添加头文件包含路径
在项目属性 -> C/C++ -> 常规 -> 附加包含目录中,添加 spdlog 的 include 路径。如果要把 spdlog 编译成库,需要在链接器中添加对应的 lib;如果使用 header-only 模式,则不需要额外链接。
4.4.3 实现文件与调试窗口双输出
这里我们会用到msvc_sink,它可以把日志输出到 Visual Studio 的 Output 窗口,对 MFC 调试非常方便。这里为了让类型兼容,将宽字符内容转为 UTF-8 后再写入 spdlog。
新建一个LogHelper.h:
#pragma once #include "spdlog/spdlog.h" #include "spdlog/sinks/basic_file_sink.h" #include "spdlog/sinks/msvc_sink.h" #include <memory> #include <string> class LogHelper { public: static void Init() { if (m_initialized) { return; } // 1. 文件 sink auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/mfc_app.log", true); // 2. MSVC 调试窗口 sink auto msvc_sink = std::make_shared<spdlog::sinks::msvc_sink_mt>(); // 3. 组合成 logger auto logger = std::make_shared<spdlog::logger>("mfc_logger", spdlog::sinks_init_list{file_sink, msvc_sink}); logger->set_level(spdlog::level::debug); logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] %v"); spdlog::set_default_logger(logger); m_initialized = true; } // 将 CString 转换为 UTF-8 字符串再输出 static void Info(const CString& msg) { std::string utf8 = CStringToUtf8(msg); spdlog::info(utf8); } static void Warn(const CString& msg) { std::string utf8 = CStringToUtf8(msg); spdlog::warn(utf8); } static void Error(const CString& msg) { std::string utf8 = CStringToUtf8(msg); spdlog::error(utf8); } private: static std::string CStringToUtf8(const CString& str) { if (str.IsEmpty()) { return ""; } #ifdef _UNICODE // 宽字符转 UTF-8 int len = WideCharToMultiByte(CP_UTF8, 0, str.GetString(), -1, nullptr, 0, nullptr, nullptr); std::string result(len - 1, '\0'); WideCharToMultiByte(CP_UTF8, 0, str.GetString(), -1, &result[0], len - 1, nullptr, nullptr); return result; #else // 多字节环境下直接返回,但需要确保源码文件保存为 UTF-8 return std::string(str.GetString()); #endif } static bool m_initialized; };在LogHelper.cpp中初始化静态变量:
#include "LogHelper.h" bool LogHelper::m_initialized = false;4.4.4 在 MFC 程序中使用
在对话框初始化函数中调用LogHelper::Init(),然后就可以在按钮事件、线程函数中使用:
void CMfcSpdlogDemoDlg::OnBnClickedButton1() { LogHelper::Init(); CString str; str.Format(_T("用户点击了按钮,参数:%d"), 100)); LogHelper::Info(str); // 模拟一个错误 LogHelper::Error(_T("数据库连接失败,错误码:0x%08X"), 1024); }这里把日志输出到 VS 的 Output 窗口,开发调试非常直观。同时文件保存为logs/mfc_app.log,用户反馈问题时可以快速收集日志。
4.4.5 MFC 集成注意事项
- 请将项目字符集设置为 Unicode,确保
CString与WideCharToMultiByte配合正常。 - 如果日志文件中出现乱码,优先检查
CStringToUtf8是否正确设置CP_UTF8,以及文件是否以 UTF-8 编码打开。 - MFC 项目中常见的
ERROR、min、max等宏可能与 spdlog 产生冲突。如果遇到宏重定义问题,可以在包含 spdlog 头文件之前定义NOMINMAX:
#ifndef NOMINMAX #define NOMINMAX #endif #include "spdlog/spdlog.h"- 不要在全局对象构造期间过早调用
LogHelper::Init(),因为 spdlog 内部可能依赖某些初始化状态。建议在InitInstance或对话框OnInitDialog中初始化。
4.5 运行与结果验证
无论使用控制台项目还是 MFC 项目,验证日志是否正常主要看两个点:
- 日志文件是否正确生成,内容是否完整。
- 控制台或调试窗口是否出现预期输出。
关于中文乱码,只要保证三处编码一致即可:
- 源码文件编码(建议 UTF-8 with BOM)。
- spdlog 写入文件时使用的编码(示例中统一转为 UTF-8)。
- 查看日志文件的编辑器编码(比如 VSCode 或 Notepad++ 使用 UTF-8 打开)。
5. 常见问题与排查思路
5.1 常见问题排查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 编译错误:找不到 spdlog.h | 头文件路径未配置 | 检查附加包含目录是否正确 |
| 编译错误:C++ 标准版本过低 | 项目编译器标准低于 C++11 | 设置 C++14 或 C++17 |
| 链接错误:无法解析外部符号 | header-only 模式与编译模式混用 | 统一使用 header-only 或编译库模式 |
| 中文日志乱码 | CString 与 UTF-8 转换不正确 | 使用WideCharToMultiByte(CP_UTF8)转换 |
| 日志没有写入文件 | 文件路径错误或权限不足 | 检查目录是否存在,程序是否具备写权限 |
| 异步日志丢失 | 程序退出前未调用 shutdown | 确保程序退出时调用spdlog::shutdown() |
| 与 Windows 宏冲突 | ERROR、min、max 等宏定义 | 定义NOMINMAX,或调整包含顺序 |
| 控制台不显示彩色信息 | Windows 旧版本控制台不支持 ANSI 转义 | 使用 MSVC 调试窗口 sink,或者输出纯文本 |
5.2 典型报错细节说明
错误 1:无法解析的外部符号
error LNK2019: 无法解析的外部符号 "class std::shared_ptr<class spdlog::logger> __cdecl spdlog::basic_logger_mt<class spdlog::sinks::basic_file_sink_mt>(...) "原因:项目同时混用了 header-only 与SPDLOG_COMPILED_LIB编译库模式。
解决方法:统一模式。如果全部使用 header-only,不要定义SPDLOG_COMPILED_LIB;如果定义了该宏,就必须链接 spdlog 库。
错误 2:min/max 宏冲突
在 Windows 下,windows.h中的min、max宏与标准库冲突,导致 spdlog 内部模板编译失败。
解决方法:
#define NOMINMAX #include "spdlog/spdlog.h"错误 3:日志文件打开失败
如果程序没有创建日志目录的权限,会抛出异常。建议在初始化之前做好目录创建和错误捕捉:
#include <filesystem> namespace fs = std::filesystem; fs::create_directories("logs"); try { auto logger = spdlog::basic_logger_mt("file_logger", "logs/app.log", true); } catch (const spdlog::spdlog_ex& ex) { // 处理日志初始化失败 }错误 4:CString 输出乱码
原因多数是编码转换问题。请严格按前面的CStringToUtf8方式转换,并确保项目字符集为 Unicode。
6. 最佳实践与工程建议
6.1 全局 logger 管理
在实际项目中,不建议在每次写日志时都创建一个新 logger,而是应该通过封装类或全局函数统一管理。前面 MFC 示例中的LogHelper就是一种做法。
更好的做法是创建一个线程安全、跨模块可用的日志单例,将所有业务模块的日志都汇总到一起。这样写日志的代码只需要一行:
LOG_INFO("用户登录成功,用户名:{}", username);可以通过宏定义来隐藏 spdlog 的细节:
#define LOG_INFO(...) spdlog::info(__VA_ARGS__) #define LOG_WARN(...) spdlog::warn(__VA_ARGS__) #define LOG_ERROR(...) spdlog::error(__VA_ARGS__) #define LOG_DEBUG(...) spdlog::debug(__VA_ARGS__)在需要记录源文件位置的场景,可以使用SPDLOG_ACTIVE_LEVEL与SPDLOG_LOGGER_CALL,但这需要一定宏配置,建议查阅对应版本的源码示例。
6.2 编译期日志级别开关
spdlog 允许在编译期定义一个激活级别,低于该级别的日志连参数都不会求值,从而获得更好的性能。
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG #include "spdlog/spdlog.h"此时可以用:
SPDLOG_LOGGER_DEBUG(logger, "调试信息,变量:{}", value);如果之后把SPDLOG_ACTIVE_LEVEL改为SPDLOG_LEVEL_INFO,则所有SPDLOG_LOGGER_DEBUG会在编译期被直接移除。这在发布版本中能显著减少日志开销。
6.3 日志文件管理建议
- 使用
rotating_logger_mt或daily_logger_mt(按天生成文件),避免单个文件无限增长。 - 定期清理过期日志,或者通过外部脚本归档。
- 日志文件路径最好可配置,比如从配置文件读取,而不是写死在代码里。
- 生产环境建议把日志目录和数据目录分开,防止日志占满磁盘影响业务。
6.4 敏感信息脱敏
日志中不要直接输出密码、token、身份证号等敏感信息。如果业务需要记录参数,建议在写入日志前做脱敏处理。
std::string maskPassword(const std::string& pwd) { if (pwd.size() <= 4) { return "****"; } return "****" + pwd.substr(pwd.size() - 4); }这是一个容易被忽略但非常重要的工程习惯。
6.5 多线程与性能建议
- 多线程环境中使用
_mt结尾的 sink,保证线程安全。 - 高频日志使用异步模型,减少主线程阻塞。
- 不要在生产环境打印超大对象或容器内容,除非确定有必要。
- 可以使用
logger->should_log(spdlog::level::debug)预先判断级别,避免构造昂贵日志参数。
示例:
if (logger->should_log(spdlog::level::debug)) { logger->debug("大型数据:{}", dumpLargeData()); }6.6 程序退出时的日志处理
使用异步日志时,必须保证程序退出前把队列中剩余日志写完。推荐在main函数末尾或进程退出钩子中调用spdlog::shutdown()。在 MFC 程序中,可以在ExitInstance中调用。
BOOL CMfcSpdlogDemoApp::ExitInstance() { spdlog::shutdown(); return CWinApp::ExitInstance(); }7. 总结与下一步
到这里,spdlog 的下载、编译、基本 API、文件轮转、异步日志、MFC 集成都已经完整过了一遍。核心要点可以归纳为几句话:
- spdlog 通过 logger + sink + formatter 的组合,把日志格式、输出目标和日志源解耦开,灵活度高。
- 生产环境中优先使用轮转文件日志,避免单文件无限增长。
- 异步日志能提升性能,但必须正确 shutdown,否则有丢失风险。
- MFC 项目集成时需要处理好 Unicode/UTF-8 转换,以及 Windows 宏冲突。
- 日志中做好脱敏、分级、路径可配置,是工程化的基本前提。
如果继续深入学习,建议按以下顺序研究:
- 阅读 spdlog 源码中
example/目录的官方示例。 - 掌握自定义 sink 与自定义 formatter 的写法。
- 熟悉
SPDLOG_ACTIVE_LEVEL等编译级宏的使用。 - 在异步模式下研究队列饱和时的丢弃策略与回调处理。
日志系统是每个项目的地基之一,早点选一个成熟的库,能帮你省下很多后面排查问题的精力。自己动手编译一个 demo,然后在真实项目中跑一周,你就能感受到 spdlog 和手写日志之间的差距了。希望这篇文章对你有帮助。