
spdlog 是 C 社区里使用率很高的一款日志库核心卖点是“header-only 也能用、性能高、上手快”。很多 C 开发者第一次接触它是为了替换自己手写的printf日志或者早期自封装日志类也有很多 MFC 项目在维护旧代码时想把日志模块从混乱的输出窗口打印切换成文件日志。这篇文章会围绕 spdlog 的安装方式、基础 API、异步日志、文件滚动、MFC 集成、常见编译错误和性能调优展开目标是把一个最小可用日志系统完整跑起来并解释清楚每一步背后的机制。spdlog 之所以受欢迎主要有三个原因。第一它支持纯头文件模式直接把include/spdlog复制到项目里就能用省去链接第三方库的麻烦。第二它基于 C11 标准实现现代编译器基本都能编译。第三它的日志输出格式、文件名滚动、异步线程池、多接收器sink设计非常清晰适合从简单到复杂的各种场景。本文会先解释 spdlog 的核心概念再给出 Windows 和 Linux 两种环境下的编译准备方式然后写一个最基础的示例再逐步加入文件日志、滚动文件、异步模式最后单独用一节说明 MFC 项目里如何集成 spdlog并提供常见编译错误速查表和排查路径。读完这篇文章你应该能独立完成一个 C 工程的日志模块改造。1. 先理解 spdlog 的日志模型再写第一行代码1.1 spdlog 中的 logger、sink、formatter 分别负责什么任何日志库都可以拆成三个职责日志接口、日志输出目标、日志格式化。spdlog 分别用 logger、sink、formatter 来表达。logger 是开发者直接操作的对象相当于一个“日志器实例”。它负责接收info()、warn()、error()等调用并判断当前日志级别是否应该输出。如果你的程序里有业务模块、网络模块、数据库模块可以给每个模块创建独立的 logger日志写入同一个文件但带上不同名字方便过滤。sink 是真正的输出终点。spdlog 提供了多种 sinksink 类型输出目标常见场景stdout_sink标准输出调试窗口、控制台程序basic_file_sink单个文件简单文件日志rotating_file_sink滚动文件按文件大小拆分daily_file_sink按天滚动文件服务端长期运行msvc_sinkVisual Studio 输出窗口Windows GUI/MFC 调试一个 logger 可以挂多个 sink比如同时输出到控制台和文件这在排查问题时会很有用。默认 logger 是spdlog::logger(default_logger)的一个全局实例直接调用spdlog::info()就是在使用它。formatter 负责把日志内容拼成最终字符串。spdlog 默认格式类似于[2025-01-01 12:00:00.123] [info] [example.cc:42] hello其中[example.cc:42]是通过source_loc拿到的源码位置需要在编译时开启SPDLOG_ACTIVE_LEVEL宏才能保证位置信息被记录。1.2 编译期级别宏与运行时级别的关系spdlog 有一个容易混淆的设计编译期级别和运行时级别是两套机制。编译期级别由SPDLOG_ACTIVE_LEVEL宏控制默认是SPDLOG_LEVEL_INFO。低于该级别的日志调用在编译阶段就会被优化掉。比如把宏设为SPDLOG_LEVEL_WARN那么spdlog::info()连函数体都不会生成这样可以减少二进制体积和运行开销。运行时级别通过logger-set_level()设置。比如开发阶段把某个模块的日志级别设为 debug生产环境再调回 info。compile-time 过滤发生在代码生成前runtime 过滤发生在每次调用前。实际项目里推荐这样处理// 在包含 spdlog.h 之前定义 #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG #include spdlog/spdlog.h #include spdlog/sinks/basic_file_sink.h如果只包含头文件不依赖 spdlog 的预编译库所有功能都会被编译进当前翻译单元首次编译会稍慢但增量编译其实还好。1.3 默认 logger 与自定义 logger 的取舍直接使用全局默认 logger 的写法是spdlog::info(Hello, {}, spdlog);这种写法适合小型工具和小型示例。它的问题在于全局唯一所有模块都共用同一个日志实例无法按模块分别控制级别或输出文件。稍微复杂一点的项目应该创建自定义 logger。创建自定义 logger 的常用方式auto console_sink std::make_sharedspdlog::sinks::stdout_color_sink_mt(); auto file_sink std::make_sharedspdlog::sinks::basic_file_sink_mt(logs/app.log, true); auto logger std::make_sharedspdlog::logger(multi_sink, std::initializer_listspdlog::sink_ptr{console_sink, file_sink}); spdlog::register_logger(logger);这里_mt后缀代表多线程安全版本。如果项目是单线程程序可以用_st版本减少锁开销但绝大多数场景直接使用_mt更稳妥因为后续接网络请求或定时器时不至于改日志库。2. 环境准备与依赖说明2.1 编译器和构建系统要求spdlog 官方要求 C11 或更高版本。较新的版本比如 1.12 以上在部分功能上要求 C17但基础日志功能 C11 即可。因此落地前一定要先看自己使用的 spdlog 版本和 README 中的标准要求。这里区分几个环境环境编译器建议标准Linux GCCg 7 及以上C11 起步建议 C17Linux Clangclang 6 及以上C11 起步建议 C17Windows MSVCVS2017 及以上C14 起步建议 C17Windows MinGW较新版本C11 起步建议 C17如果项目里已经使用了 C17直接使用-stdc17编译不会有什么问题。如果还在维护 C11 老项目就要选择与 C11 兼容的 spdlog 版本或者谨慎升级。2.2 方式一纯头文件模式下载 spdlog 源码后目录结构大致如下spdlog/ include/ spdlog/ spdlog.h ... src/ spdlog.cpp ... CMakeLists.txt在 CMake 工程中header-only 模式最简单的引用方式add_executable(demo main.cpp) target_include_directories(demo PRIVATE path/to/spdlog/include) target_compile_features(demo PRIVATE cxx_std_17)如果使用 g 直接编译g -stdc17 -I./spdlog/include main.cpp -o demoheader-only 模式不需要链接额外的.a或.lib文件因为日志库的所有实现都在头文件里。但编译单元会膨胀首次编译时间偏长。2.3 方式二预编译源码模式如果希望减少编译时间或者需要把 spdlog 编译成动态库/静态库可以编译src/spdlog.cpp。使用 CMake 时可以让项目启用SPDLOG_COMPILED_LIBset(SPDLOG_COMPILED_LIB ON) add_subdirectory(path/to/spdlog) target_link_libraries(demo PRIVATE spdlog::spdlog)也可以手动在源文件中包含编译实现#include spdlog/spdlog.h // 在某个 .cpp 里加入 #include spdlog/sinks/basic_file_sink.cpp不过这种手动包含.cpp的方式不够优雅推荐使用 CMake 的add_subdirectory。2.4 方式三包管理器安装在 Linux 上使用 vcpkg 或 apt 都可以。需要说明apt 安装的 spdlog 版本可能较旧如果项目必须使用新特性建议源码集成。# vcpkg vcpkg install spdlog # Ubuntu/Debian 示例 sudo apt install libspdlog-dev使用 vcpkg 时CMake 需要通过 toolchain 文件找到包cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE[vcpkg-root]/scripts/buildsystems/vcpkg.cmake重点提醒如果项目使用纯头文件模式不要同时链接spdlog::spdlog编译库否则可能出现符号重复定义或 ABL 不匹配的链接错误。选择一个模式并保持一致性。3. 第一个可运行示例控制台输出与基本格式化3.1 最小控制台示例创建main.cpp#include spdlog/spdlog.h int main() { spdlog::info(Welcome to spdlog); spdlog::warn(This is a warning, value {}, 42); spdlog::error(Error code: {}, -1); return 0; }编译并运行g -stdc17 -I./spdlog/include main.cpp -o demo ./demo预期输出类似于[2025-01-01 10:00:00.123] [info] Welcome to spdlog [2025-01-01 10:00:00.123] [warning] This is a warning, value 42 [2025-01-01 10:00:00.123] [error] Error code: -1这里有一个重要细节spdlog::warn()的日志级别名称在控制台显示为[warning]这是默认 pattern 的规则。如果你在文件里看到[warning]不要觉得是拼写错误。3.2 占位符语法与常见误用spdlog 使用{}占位符和 C20 的std::format风格类似。不要在{}内部写%d、%s那会原样输出。int id 100; std::string name order; spdlog::info(id{}, name{}, id, name); // 错误示例spdlog::info(id%d, name%s, id, name);占位符数量必须和参数数量一致。参数少一个不会在编译期报错但会在运行时输出格式异常或抛出异常。生产代码中如果不确定优先把日志拆分避免一条日志里拼太多变量。3.3 自定义 pattern 的推荐格式日志格式会影响日志排查效率。推荐包含时间、级别、线程 id、源码位置、消息内容spdlog::set_pattern([%Y-%m-%d %H:%M:%S.%e] [%t] [%l] [%s:%#] %v);常见 pattern 标记速查标记含义%Y年%m月%d日%H:%M:%S时分秒%e毫秒%t线程 id%l日志级别%s源文件名%#源文件行号%v日志正文源码位置信息只有在调用SPDLOG_LOGGER_INFO这类宏时才会被正确填充。如果直接调用logger-info()默认也能拿到位置但宏方式可以在编译期就过滤掉低级别日志性能更好。实际项目中常用宏封装#define LOG_INFO(...) SPDLOG_LOGGER_INFO(::spdlog::default_logger(), __VA_ARGS__) #define LOG_ERROR(...) SPDLOG_LOGGER_ERROR(::spdlog::default_logger(), __VA_ARGS__)如果希望每个模块有自己的 logger可以把宏改成传入 logger 指针的版本。4. 文件日志basic_file_sink 与 rotating_file_sink4.1 基础文件日志单文件日志适合开发阶段或短时间运行的脚本。使用basic_file_sink_mt时第二个参数是是否追加。#include spdlog/spdlog.h #include spdlog/sinks/basic_file_sink.h int main() { auto file_logger spdlog::basic_logger_mt(file_logger, logs/app.log, true); spdlog::set_default_logger(file_logger); file_logger-info(write to file); file_logger-flush_on(spdlog::level::info); return 0; }flush_on表示在指定级别时立即刷新缓冲区。如果程序崩溃没有刷新到磁盘的日志会丢失。生产环境至少把error级别设置为自动刷新同时定期调用logger-flush()。4.2 滚动文件日志长时间运行的服务如果只写单个文件磁盘会被撑满排查时也不方便定位某一天的日志。spdlog 的rotating_file_sink在文件达到指定大小后滚动生成新文件并保留最多一定数量的历史文件。#include spdlog/spdlog.h #include spdlog/sinks/rotating_file_sink.h int main() { // 文件名为 app.log单个文件最大 5MB最多保留 3 个历史文件 auto logger spdlog::rotating_logger_mt(rotating_logger, logs/rotate.log, 1024 * 1024 * 5, 3); logger-info(rolling file log); return 0; }滚动后文件会变成类似rotate.log rotate.1.log rotate.2.log rotate.3.logmax_size参数过小会导致频繁滚动过大会导致单文件太大。建议根据单条日志平均大小和日志量估算。例如每分钟 1MB 日志单文件设 100MB 比较合理。4.3 按天滚动文件日志服务端程序通常适合按天滚动保留最近 N 天日志。daily_file_sink需要指定几点进行切换。#include spdlog/spdlog.h #include spdlog/sinks/daily_file_sink.h int main() { auto logger spdlog::daily_logger_mt(daily_logger, logs/daily.log, 0, 0); logger-info(daily log message); return 0; }daily_file_sink的构造函数参数是时、分0 点 0 分切换文件。它会生成类似daily_2025-01-01.log的文件。按天滚动在排查线上问题时非常有用比如直接查看某一天的日志文件。4.4 多 sink 组合同时写控制台和文件很多时候调试时需要看到控制台输出同时保存文件日志。可以用sinks初始化列表组合多个 sink。#include spdlog/spdlog.h #include spdlog/sinks/stdout_color_sinks.h #include spdlog/sinks/rotating_file_sink.h int main() { auto console_sink std::make_sharedspdlog::sinks::stdout_color_sink_mt(); auto file_sink std::make_sharedspdlog::sinks::rotating_file_sink_mt(logs/multi.log, 1024 * 1024 * 5, 3); std::vectorspdlog::sink_ptr sinks{console_sink, file_sink}; auto logger std::make_sharedspdlog::logger(multi, sinks.begin(), sinks.end()); spdlog::set_default_logger(logger); logger-info(multi sink message); return 0; }此时同一条日志会同时写入控制台和文件文件里不包含 ANSI 颜色码因为颜色是由stdout_color_sink_mt单独处理的。这个设计在 spdlog 里非常清晰sink 之间互不干扰。5. 异步日志与性能调优5.1 为什么需要异步日志同步日志在调用logger-info()时会直接执行格式化、sink 写入、可能还有文件 flush。如果日志量大日志写入会拖慢业务线程。异步日志会把日志消息先放入队列再由后台线程统一写入文件业务线程只负责放入队列可以显著减少日志对业务逻辑的影响。spdlog 提供async_logger。使用前需要初始化一个thread_pool#include spdlog/spdlog.h #include spdlog/async.h #include spdlog/sinks/rotating_file_sink.h int main() { // 队列大小 8192后台线程数 1 spdlog::init_thread_pool(8192, 1); auto file_sink std::make_sharedspdlog::sinks::rotating_file_sink_mt(logs/async.log, 1024 * 1024 * 5, 3); auto logger std::make_sharedspdlog::async_logger( async_logger, file_sink, spdlog::thread_pool(), spdlog::async_overflow_policy::block); spdlog::set_default_logger(logger); for (int i 0; i 1000; i) { logger-info(async message {}, i); } spdlog::shutdown(); return 0; }5.2 队列溢出策略异步日志队列是有限的。如果日志产生速度大于后台线程消费速度队列会被塞满。async_overflow_policy有两种常见策略策略行为适用场景block队列满时业务线程阻塞等待直到有空间日志不能丢但可能拖慢业务overrun_oldest丢弃最旧的日志保证新日志能入队日志可以丢但要保证最新日志生产环境建议使用block作为默认策略同时把队列大小设置得足够大。如果担心日志丢失可以在shutdown()前调用logger-flush()。5.3 同步与异步的性能表现性能测试结果依赖硬件、线程数、日志格式和磁盘类型。spdlog 官方描述其同步日志性能很高异步日志适合吞吐量大的场景。不要盲信任何一个具体数值最稳妥的做法是在自己项目里用相同日志格式、相同输出路径做压测对比业务线程的耗时和日志吞吐量。异步模式的代价是实时性下降。程序崩溃时队列中尚未写入的日志会丢失。因此在需要立刻看到日志的场景比如热修复、金额计算、安全关键逻辑不要把这些日志设计成异步或至少同时追加一条同步的错误日志。5.4 避免在日志参数里做昂贵计算即使使用异步日志参数表达式仍然会在调用线程求值。例如logger-info(user info: {}, build_large_json(user));build_large_json(user)在进入日志库前就会执行。如果这个函数很昂贵日志系统再快也没用。建议先判断级别再组装日志内容if (logger-should_log(spdlog::level::debug)) { logger-debug(user info: {}, build_large_json(user)); }或者使用SPDLOG_LOGGER_DEBUG宏它可以配合编译期级别在编译阶段移除低级别日志。6. MFC 项目集成 spdlog示例代码与 Visual Studio 配置6.1 MFC 项目为什么需要日志库MFC 项目通常是 Windows 桌面程序很多老代码使用OutputDebugString、TRACE、AfxMessageBox输出运行信息。这些方式在发布版本里不适合定位问题OutputDebugString需要调试器才能看到AfxMessageBox会阻塞 UITRACE在 Release 下默认无效。用 spdlog 把日志写入文件可以在用户机器上收集运行轨迹排查问题更高效。6.2 配置 Visual Studio 工程在 VS 中集成 spdlog 的步骤将 spdlog 源码放到项目目录比如external/spdlog。项目属性 - C/C - 常规 - 附加包含目录添加external/spdlog/include。如果使用预编译头建议把 spdlog 头文件包含在stdafx.h或pch.h中避免每个.cpp重复包含。语言标准设置为 C17路径在 项目属性 - C/C - 语言 - C 语言标准。如果使用纯头文件模式不需要配置链接库。如果已经编译了spdlog.lib或使用 vcpkg则需要在链接器输入中添加库文件。6.3 MFC 项目中的示例代码以下是一个模拟 MFC 对话框工程中初始化和使用日志的例子。在stdafx.h或pch.h中加入#pragma once #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG #include spdlog/spdlog.h #include spdlog/sinks/basic_file_sink.h #include spdlog/sinks/msvc_sink.h在CWinApp::InitInstance中初始化日志BOOL CMyApp::InitInstance() { // 配置输出到 Visual Studio 输出窗口和文件 auto msvc_sink std::make_sharedspdlog::sinks::msvc_sink_mt(); auto file_sink std::make_sharedspdlog::sinks::rotating_file_sink_mt( logs/myapp.log, 1024 * 1024 * 5, 5); std::vectorspdlog::sink_ptr sinks{msvc_sink, file_sink}; auto logger std::make_sharedspdlog::logger(mfc_app, sinks.begin(), sinks.end()); logger-set_level(spdlog::level::debug); logger-flush_on(spdlog::level::warn); spdlog::set_default_logger(logger); spdlog::set_pattern([%Y-%m-%d %H:%M:%S.%e] [%t] [%l] [%s:%#] %v); LOG_INFO(MFC application started); return CWinApp::InitInstance(); }在业务代码中使用void CMyDialog::OnBnClickedButton() { try { // 业务逻辑 LOG_INFO(button clicked); } catch (const std::exception e) { LOG_ERROR(exception: {}, e.what()); } }msvc_sink输出到 VS 的“输出”窗口方便开发时调试rotating_file_sink写入发布环境的日志文件。这样开发环境和用户现场都能保留日志。6.4 MFC 中要注意编码问题spdlog 默认按 UTF-8 处理字符串。MFC 项目中如果使用CString在日志输出前需要显式转换尤其是 Unicode 字符集_UNICODE下CString是宽字符类型不能直接作为{}参数传入 spdlog。推荐封装一层转换函数#include atlstr.h std::string ToUtf8(const CString str) { if (str.IsEmpty()) { return std::string(); } int len WideCharToMultiByte(CP_UTF8, 0, str.GetString(), str.GetLength(), nullptr, 0, nullptr, nullptr); std::string result(len, \0); WideCharToMultiByte(CP_UTF8, 0, str.GetString(), str.GetLength(), result[0], len, nullptr, nullptr); return result; } // 使用示例 CString userName L张三; LOG_INFO(user name: {}, ToUtf8(userName));如果不做转换CString在 spdlog 的格式化机制里可能被当成指针或字符数组处理轻则输出乱码重则导致崩溃。这是 MFC 集成中最常见的坑之一。6.5 MFC 程序中关闭时的清理顺序如果直接在ExitInstance或窗口销毁代码中调用spdlog::shutdown()要注意 logger 可能还被其他线程使用。建议在进程即将退出、所有业务线程已经释放后再调用int CMyApp::ExitInstance() { spdlog::shutdown(); return CWinApp::ExitInstance(); }shutdown()会停止异步线程池并刷新所有 logger。如果先退出了异步线程再调用日志接口可能出现“logger 已关闭”的异常。因此要保证退出路径上不再有任何日志调用。7. 常见报错、排查路径与解决方案7.1 编译错误找不到头文件现象fatal error C1083: Cannot open include file: spdlog/spdlog.h: No such file or directory检查顺序确认 spdlog 的include目录路径是否写对。常见错误是把spdlog/include/spdlog路径加入 include导致编译器去查找spdlog/spdlog/spdlog.h。确认附加包含目录是否配置到spdlog/include这一层。如果项目开启了预编译头确认头文件是否在预编译头中正确包含。推荐做法// 错误示例 #include spdlog/spdlog.h // 检查 include 路径 // 推荐确认目录结构后配置成 // 附加包含目录D:\project\external\spdlog\include7.2 链接错误符号重复定义如果同时启用了SPDLOG_HEADER_ONLY和编译库可能出现重复符号。现象类似LNK2005 spdlog::logger::log already defined in spdlog.lib原因spdlog 的函数既在头文件中有 inline 定义又在编译后的库中有非 inline 定义。解决方案不要混合使用。纯头文件模式不要链接编译库编译库模式不要定义SPDLOG_HEADER_ONLY。7.3 日志文件没有生成或为空可能原因和排查顺序现象可能原因检查方式处理方案没有生成文件文件路径目录不存在查看当前工作目录先创建目录或使用绝对路径文件生成为空日志级别过滤检查set_level和flush_on调低级别或手动 flush文件刷新不及时异步队列未刷盘程序退出前未 shutdown调用spdlog::shutdown()文件权限不足程序没有目录写权限检查日志目录 ACL修改目录权限或更换路径7.4 运行时异常spdlog_exspdlog 在无法创建文件、无法打开 sink 时会抛出spdlog::spdlog_ex异常。现象terminate called after throwing an instance of spdlog::spdlog_ex what(): Failed to open file ...解决方案启动前创建日志目录。使用 try-catch 捕获初始化异常避免程序直接崩溃。try { auto logger spdlog::basic_logger_mt(app, logs/app.log); spdlog::set_default_logger(logger); } catch (const spdlog::spdlog_ex e) { // 处理初始化失败例如回退到控制台日志 spdlog::set_default_logger(spdlog::stdout_logger_mt(console)); spdlog::error(Failed to create file logger: {}, e.what()); }7.5 日志时间显示不正确spdlog 默认使用本地时间UTC 场景需要设置set_pattern中的时间戳来源。如果不特殊设置%Y-%m-%d %H:%M:%S输出的是本机时间。在容器或服务器环境中时区不对会导致日志时间和事件时间不一致。排查方式先对比本机date命令输出和日志时间确认是否有几小时偏差。如果是 UTC 日志需求使用 spdlog 的custom_flag_formatter或自定义 pattern尽量在程序初始化时统一设置时区处理。7.6 中文乱码中文乱码通常源于编码不一致。比如源文件是 GBK而日志文件按 UTF-8 写入。建议统一采用 UTF-8 编码源文件保存为 UTF-8。使用/utf-8编译选项VS 中可在 项目属性 - C/C - 命令行 添加/utf-8。MFC 中的CString显式转 UTF-8。如果是老项目历史遗留的 GBK 日志则要保持输出编码与查看工具一致不要混用。8. 生产环境最佳实践与扩展方向8.1 日志目录和文件命名规范生产环境日志目录应独立于程序安装目录避免升级卸载时丢失日志。推荐结构/var/log/myapp/ myapp_2025-01-01.log myapp_2025-01-02.logWindows 服务程序可以放在C:\ProgramData\MyApp\logs而不是安装目录。程序启动时通过GetModuleFileName或配置文件路径拼出日志目录不要在代码里硬编码“当前目录”。8.2 日志级别切换的工程手段不要在发布版本里手动改代码来切换日志级别。建议从配置文件或环境变量读取。std::string level_str GetConfigValue(log_level, info); spdlog::level::level_enum level spdlog::level::from_str(level_str); logger-set_level(level);这样可以做到不动代码、不重新编译在用户现场把日志级别临时调低定位问题后再调回。8.3 日志脱敏用户信息、token、密钥、手机号、身份证号不能直接写入日志。可以在封装层做脱敏处理std::string MaskPhone(const std::string phone) { if (phone.size() 7) { return ****; } return phone.substr(0, 3) **** phone.substr(7); }然后在日志输出前调用。8.4 日志监控与告警文件日志只是基础生产环境还需要采集和告警。spdlog 输出的结构化格式如果设计得好可以通过 filebeat、fluentd 等工具采集到日志平台。关键点固定 pattern方便解析。使用 JSON sink 输出结构化日志。记录 trace id、request id 用于链路追踪。spdlog 本身不包含日志采集和告警能力但它的 sink 开放接口允许接入自定义 sink。比如可以写一个 HTTP sink 把错误日志推送到内网告警服务。8.5 自定义 sink 的扩展思路如果项目需要把日志写入数据库、发送到消息队列、转发到远程日志服务可以实现spdlog::sinks::base_sink。#include spdlog/sinks/base_sink.h class my_http_sink final : public spdlog::sinks::base_sinkstd::mutex { protected: void sink_it_(const spdlog::details::log_msg msg) override { // 将 msg 格式化并发送到远程 } void flush_() override { // 刷新缓冲区 } };自定义 sink 时要注意线程安全和性能不要在sink_it_里做耗时太长的远程调用否则会阻塞 logger 或异步队列。8.6 建议的学习路径和练习清单想真正掌握 spdlog建议按顺序做下面几个练习用 header-only 模式写一个控制台日志程序。加入滚动文件日志模拟生成 10000 条日志。设置自定义 pattern包含线程 id 和源码位置。使用异步模式观察日志吞吐量和程序退出时的刷新行为。在 MFC 或 Qt 程序中集成 spdlog同时输出到调试窗口和文件。编写一个自定义 sink把 error 级别日志单独写入另一个文件。在项目里新增日志脱敏工具类封装spdlog::info等宏。完成这些练习后你会对 sink、logger、异步队列、编译期级别、运行时级别这些概念形成自己的理解后续遇到日志丢失、文件滚动异常、编码乱码等问题时也能更快定位到具体环节。9. 总结与关键选择spdlog 不是一个很复杂的技术但它的设计选择值得认真理解。对大多数项目推荐使用纯头文件模式起步把日志封装在统一模块中用rotating_file_sink或daily_file_sink作为文件输出同时挂一个调试窗口 sink 方便开发。等到日志量增大再切换到异步模式并根据业务特点选择队列溢出策略。最重要的一点是日志系统不是写完初始化代码就结束了它需要根据实际运行情况持续调整。日志文件增长是否过快、异步队列是否经常满、日志级别是否需要远程切换、敏感信息是否被写入文件这些都是上线后要持续观察的问题。先在开发环境把日志链路完整跑通再逐步加上文件滚动、异步、脱敏和监控才是比较稳妥的推进方式。