拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C++日志库spdlog从入门到工程落地:集成、异步与MFC实战

在 C 后端服务、桌面客户端和游戏项目中日志模块总是绕不开的一环。很多团队早期为了省事直接用printf、OutputDebugString或者自己封装一个简单文件写入类来记录日志。等项目体量变大、并发变高、线上问题变多之后才发现日志系统补丁摞补丁性能跟不上格式不统一还经常出现多线程写入错乱、日志文件被占用、磁盘被写满等尴尬问题。本文将围绕 GitHub 上非常热门的 C 日志库spdlog作者 gabime展开结合“spdlog 下载”“MFC 使用 spdlog 例子代码”等高频搜索词整理一份从入门到工程落地的完整教程覆盖安装方式、核心概念、基础用法、异步日志、MFC 集成与常见踩坑排查。1. 为什么选择 spdlogC 日志库的标杆1.1 C 项目手写日志的痛点在没有引入第三方日志库之前很多 C 项目里的日志模块是这样发展的早期printf 重定向到文件或者直接输出到控制台中期封装一个Log::Write静态方法内部打开文件、写入、关闭后期随着线程增多发现写入会互相覆盖于是加锁接着发现日志文件越来越大又写一个 Split 逻辑再后来需要按天归档还得再改。这套手写方案的核心问题不是“能不能跑”而是维护成本和可靠性。比如频繁打开关闭文件会导致性能下降多线程日志拼接时容易产生乱码日志刷盘时机不合适程序崩溃时最后几条日志丢失格式不统一后期想用脚本分析日志非常麻烦。1.2 spdlog 是什么spdlog 是一个基于 C11 实现的快速、header-only也可编译为库的日志库。它在 GitHub 上由开发者 gabime 维护star 数量在 C 日志库中遥遥领先。之所以叫 spdlog可以理解为 speed log即“高性能日志”。spdlog 用 C11 编写支持 C11/14/17/20 等主流标准在 Windows、Linux、macOS 上都可以使用。它提供了丰富的日志目的地称为 sink例如控制台stdout / stderr基础文件轮转文件按大小切割每日文件按时间切割系统日志syslogWindows 调试输出OutputDebugStringMSVC 调试窗口自定义 sink由于源码实现精炼、API 设计现代spdlog 非常适合嵌入到各类 C 项目中。1.3 spdlog 的核心特性我梳理了几个开发者最关心的特性特性说明高性能使用预格式化、批量写入、异步模式性能远高于手写日志Header-only默认只需包含头文件集成简单无需第三方依赖仅依赖 C 标准库格式化功能强大使用 fmt 风格的格式化语法spdlog 内置了 fmt 子集多种 Sink控制台、文件、轮转、每日、MSVC 调试输出等异步日志通过线程池批量写日志降低业务线程阻塞宽字符支持支持 wchar_t 日志Windows 下常用可扩展自定义 sink、自定义格式化器在实际工程中使用 spdlog最大的收益不是“写日志”本身而是得到了一个设计完整、可隔离、可配置、可监控的日志基础设施。2. spdlog 下载与工程集成2.1 下载 spdlog 源码搜索“spdlog 下载”时大家通常想找的是源码包或二进制包。spdlog 的官方仓库位于 GitHubgabime/spdlog。你可以用 Release 页面下载源码压缩包也可以直接用 git clonegit clone https://github.com/gabime/spdlog.git cd spdlog如果你不想手动下载也可以使用包管理器# vcpkg vcpkg install spdlog # Conan conan install spdlog/1.14.1具体版本号会持续更新建议以官方仓库 Release 信息为准。本文示例以安装源码后编译集成为主思路适用于大多数版本。2.2 Header-only 模式集成spdlog 默认是 header-only 模式这是最方便的集成方式。你只需要把include目录添加到项目的头文件搜索路径中然后直接包含头文件即可#include spdlog/spdlog.h #include spdlog/sinks/basic_file_sink.h例如 Visual Studio 工程中在“C/C - 常规 - 附加包含目录”里添加 spdlog 源码的include路径D:\third_party\spdlog\include在 CMake 项目中可以使用FetchContent或add_subdirectory引入cmake_minimum_required(VERSION 3.14) project(SpdlogDemo) set(CMAKE_CXX_STANDARD 11) # 方式1通过 FetchContent 自动拉取 include(FetchContent) FetchContent_Declare( spdlog GIT_REPOSITORY https://github.com/gabime/spdlog.git GIT_TAG v1.14.1 ) FetchContent_MakeAvailable(spdlog) add_executable(demo main.cpp) target_link_libraries(demo PRIVATE spdlog::spdlog)使用 header-only 模式时不需要链接额外的.lib或.a文件编译期会直接展开实现代码。2.3 编译成静态库或动态库如果项目较大为了减少编译时间也可以把 spdlog 编译成静态库或动态库。spdlog 提供了 CMake 工程直接构建即可cd spdlog mkdir build cd build cmake .. -DSPDLOG_BUILD_SHAREDOFF cmake --build . --config Release编译后在链接阶段需要添加include路径链接spdlog库文件在代码中定义宏SPDLOG_COMPILED_LIB。例如 Visual Studio 中预处理定义添加SPDLOG_COMPILED_LIB2.4 集成到项目时的目录结构建议无论使用哪种方式建议在工程中单独管理第三方库third_party/ spdlog/ include/ src/ CMakeLists.txt这样后期升级 spdlog 时只需要替换整个目录不影响业务代码。3. spdlog 核心概念与配置拆解3.1 Logger、Sink、Formatter 的关系spdlog 的架构核心是三个组件Logger日志记录器对外提供info()、warn()、error()等方法负责构建日志消息。Sink日志输出目标决定日志写到哪比如控制台、文件、轮转文件等。Formatter格式化器决定日志的展示格式比如时间戳、线程号、日志级别、消息体等。一个 Logger 可以同时挂多个 Sink。比如你希望日志既输出到控制台又写入本地文件就可以创建两个 Sink 并注册到同一个 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, spdlog::sinks_init_list{console_sink, file_sink});这种设计让日志的“写什么”和“写到哪”彻底解耦扩展新输出目标时不需要改动业务代码。3.2 日志级别spdlog 定义了完整的日志级别从低到高依次为trace debug info warn err critical off对应的方法名logger-trace(trace message); logger-debug(debug message); logger-info(info message); logger-warn(warn message); logger-error(error message); logger-critical(critical message);通过设置全局级别或 logger 级别可以灵活控制日志输出量。比如开发阶段设置为debug线上设置为infospdlog::set_level(spdlog::level::debug); // 或者只设置某个 logger logger-set_level(spdlog::level::warn);级别过滤是在日志消息构造之前就发生的因此被过滤掉的日志不会带来明显的性能开销。3.3 Pattern 格式串spdlog 使用 pattern 控制日志输出格式核心标识符如下占位符含义%Y%m%d年、月、日%H%M%S时、分、秒%e毫秒%t线程 ID%nLogger 名称%l日志级别小写%L日志级别大写%v消息内容%s%#%!源文件名、行号、函数名需要开启%%百分号本身示例spdlog::set_pattern([%Y-%m-%d %H:%M:%S.%e] [%t] [%l] %v);输出效果[2025-01-15 10:24:33.123] [12345] [info] user login success如果需要同时打印文件名和行号代码中要使用源位置宏spdlog::set_pattern([%H:%M:%S] [%s:%#] [%l] %v); logger-info(login success);这会输出类似[10:24:33] [main.cpp:23] [info] login success3.4 Flush 策略与性能spdlog 默认会在消息到达时写入 sink但是否立刻刷盘取决于 flush 策略。高频日志场景下每一条都刷盘会严重拉低性能因此 spdlog 提供了两种控制方式// 方式1定期自动 flush spdlog::flush_every(std::chrono::seconds(3)); // 方式2某个级别及以上立即 flush spdlog::flush_on(spdlog::level::warn);推荐组合是低级别日志批量写入警告及以上日志立即刷盘确保关键错误不丢失。4. spdlog 基础使用实战4.1 控制台日志先来看一个最简单的示例。新建main.cpp// 文件路径src/main.cpp #include spdlog/spdlog.h int main() { // 设置全局格式 spdlog::set_pattern([%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [thread %t] %v); spdlog::info(Welcome to spdlog!); spdlog::warn(This is a warning message, value {}, 42); spdlog::error(Failed to open file: {}, config.ini); return 0; }说明spdlog::info等全局函数内部使用了默认 logger名为default%^和%$用于给日志级别着色控制台下显示更直观{}是 fmt 风格的占位符不需要像printf那样严格匹配类型编译运行后控制台会输出带颜色的日志。4.2 写入基础文件文件日志是生产环境最常见的需求。使用basic_file_sink_mt创建一个基础文件 logger// 文件路径src/file_logger.cpp #include spdlog/spdlog.h #include spdlog/sinks/basic_file_sink.h int main() { // 参数文件名是否截断 auto logger spdlog::basic_logger_mt(file_logger, logs/app.log, true); logger-set_level(spdlog::level::debug); logger-set_pattern([%Y-%m-%d %H:%M:%S.%e] [%l] %v); logger-debug(debug message); logger-info(info message); logger-warn(warn message); spdlog::shutdown(); // 关闭所有 logger释放资源 return 0; }注意basic_logger_mt的第三个参数为true时表示启动时截断旧文件即重新开始写。生产环境建议设为false保留历史记录。这里带_mt后缀表示多线程安全multi-thread。spdlog 也提供_st后缀的单线程版本适合明确无并发访问的场景。4.3 轮转日志与每日日志当业务日志量较大时默认的单一文件会越来越大既不便于查看也不便于归档。spdlog 提供了两种官方解决方案。第一种是轮转文件日志按文件大小切割#include spdlog/sinks/rotating_file_sink.h auto logger spdlog::rotating_logger_mt( rotating_logger, logs/rotate.log, 1024 * 1024 * 5, // 每个文件最大 5MB 3 // 最多保留 3 个文件 );当rotate.log超过 5MB 后spdlog 会依次重命名并生成新的日志文件。第二种是每日日志按日期切换文件#include spdlog/sinks/daily_file_sink.h auto logger spdlog::daily_logger_mt( daily_logger, logs/daily.log, 0, // 凌晨 0 点 0 // 0 分切换 );这样日志文件会按天命名为daily.log、daily.2025-01-15.log等形式非常适合长期运行的服务。4.4 异步日志当业务线程处于高吞吐、高并发状态时如果每次写日志都要同步操作文件线程会被磁盘 IO 拖慢。spdlog 的异步模式可以解决这个问题。使用异步日志前需要先初始化线程池#include spdlog/async.h #include spdlog/sinks/basic_file_sink.h int main() { // 初始化线程池队列大小 8192工作线程数量 1 spdlog::init_thread_pool(8192, 1); // 创建异步 logger auto logger spdlog::create_asyncspdlog::sinks::basic_file_sink_mt( async_logger, logs/async.log, true ); logger-info(async log message); // 程序退出前确保异步队列刷出 spdlog::shutdown(); return 0; }异步日志的核心思路是业务线程把日志消息放入内存队列后立即返回后台工作线程从队列取消息并写入目标 sink。这样做能显著降低业务线程的写日志开销但需要注意队列大小要合理避免极端情况下消息丢失shutdown()会等待队列清空建议在进程退出前调用。4.5 自定义格式与多 Sink 组合实际项目中往往需要同时输出到控制台和文件并且两个目标的格式可能不同。例如控制台带颜色方便调试文件使用纯文本方便采集分析。可以通过分别设置 formatter 实现#include spdlog/sinks/stdout_color_sinks.h #include spdlog/sinks/basic_file_sink.h auto console_sink std::make_sharedspdlog::sinks::stdout_color_sink_mt(); console_sink-set_pattern([%H:%M:%S] [%^%l%$] %v); auto file_sink std::make_sharedspdlog::sinks::basic_file_sink_mt(logs/app.log, true); file_sink-set_pattern([%Y-%m-%d %H:%M:%S.%e] [%l] [thread %t] %v); std::vectorspdlog::sink_ptr sinks{console_sink, file_sink}; auto logger std::make_sharedspdlog::logger(multi, sinks.begin(), sinks.end()); spdlog::register_logger(logger); logger-info(message with different formats);5. MFC 项目中使用 spdlog 完整示例搜索“MFC 使用 spdlog 例子代码”的开发者通常是希望在桌面 Windows 程序中接入日志系统。MFC 项目相比普通控制台项目有一些特殊地方需要处理。5.1 MFC 与 spdlog 的适配要点MFC 使用的是 Unicode 字符集CString内部为wchar_t而 spdlog 默认处理的是char*/std::string。因此直接写logger-info(cstr)会编译报错。解决方案有两个方向将CString转成std::string后传入 spdlog使用 spdlog 的宽字符支持创建wchar_t类型的 logger。从工程易用性来看方案 1 更简单直接也方便后续日志采集和传输。下面统一采用方案 1 说明。5.2 创建全局日志管理器MFC 程序通常有多个类CWinApp、CDialog、CView等需要写日志建议封装一个全局的日志管理类避免每个类各自创建 logger。新建LogManager.h// 文件路径MfcSpdlogDemo/LogManager.h #pragma once #include spdlog/spdlog.h #include spdlog/sinks/rotating_file_sink.h class LogManager { public: // 初始化全局日志系统 static bool Init(const std::string logDir logs); // 获取全局 logger static std::shared_ptrspdlog::logger GetLogger(); private: static std::shared_ptrspdlog::logger s_logger; };新建LogManager.cpp// 文件路径MfcSpdlogDemo/LogManager.cpp #include LogManager.h #include filesystem std::shared_ptrspdlog::logger LogManager::s_logger nullptr; bool LogManager::Init(const std::string logDir) { try { // 确保日志目录存在 std::filesystem::create_directories(logDir); std::string logFile logDir /App.log; // 使用轮转文件每个文件 5MB保留 5 个文件 s_logger spdlog::rotating_logger_mt( mfc_app_logger, logFile, 5 * 1024 * 1024, 5 ); s_logger-set_level(spdlog::level::debug); s_logger-set_pattern([%Y-%m-%d %H:%M:%S.%e] [%l] [thread %t] %v); s_logger-flush_on(spdlog::level::warn); // warning 及以上立即刷盘 return true; } catch (const spdlog::spdlog_ex ex) { // 日志初始化失败时直接输出到调试窗口 OutputDebugStringA(ex.what()); return false; } } std::shared_ptrspdlog::logger LogManager::GetLogger() { return s_logger; }注意std::filesystem在 MFC 项目中需要确认 C 标准为 C17 或更高。如果项目还在使用 C14可以用 Windows API 的CreateDirectory替代。5.3 在 MFC 类中调用在CWinApp::InitInstance中初始化日志系统// 文件路径MfcSpdlogDemo/MfcSpdlogDemo.cpp BOOL CMfcSpdlogDemoApp::InitInstance() { // 初始化日志系统 if (!LogManager::Init(logs)) { AfxMessageBox(_T(初始化日志系统失败)); } // 记录程序启动日志 LogManager::GetLogger()-info(Application started); // 后续业务代码…… }在对话框或视图类中记录业务日志void CMainDialog::OnBnClickedLogin() { CString username; m_editUsername.GetWindowText(username); // CString 转 std::string CT2A ascii(username); std::string strUser(ascii); LOG_INFO(User login: {}, strUser); // 执行登录逻辑…… }为了减少代码噪音可以在项目公共头文件中定义一组短宏// 文件路径MfcSpdlogDemo/LogMacro.h #pragma once #include LogManager.h #define LOG_TRACE(...) LogManager::GetLogger()-trace(__VA_ARGS__) #define LOG_DEBUG(...) LogManager::GetLogger()-debug(__VA_ARGS__) #define LOG_INFO(...) LogManager::GetLogger()-info(__VA_ARGS__) #define LOG_WARN(...) LogManager::GetLogger()-warn(__VA_ARGS__) #define LOG_ERROR(...) LogManager::GetLogger()-error(__VA_ARGS__)这样业务代码中写日志就非常简洁LOG_INFO(User {} login success, ip {}, strUser, strIp);如果希望同时输出到 IDE 的“输出”窗口还可以在追加一个 MSVC sink#include spdlog/sinks/msvc_sink.h auto msvc_sink std::make_sharedspdlog::sinks::msvc_sink_mt(); msvc_sink-set_pattern([%H:%M:%S.%e] [%l] %v); // 将 msvc_sink 添加到 logger 的 sink 列表这样调试 MFC 程序时日志会在 Visual Studio 的输出窗口中实时显示非常方便。5.4 MFC 使用 spdlog 的典型代码结构汇总一下 MFC 项目中使用 spdlog 的完整代码流程下载 spdlog 源码加入到项目的 include 路径封装LogManager单例工具类在InitInstance中调用LogManager::Init定义日志宏在业务代码中使用在ExitInstance中调用spdlog::shutdown()。6. 常见问题与排查思路spdlog 整体 API 设计比较友好但在实际使用中尤其是 MFC / Windows 项目里仍然有一些高频问题。下面整理成表格方便查阅。问题现象常见原因解决思路编译时报错“无法打开包含文件 spdlog/spdlog.h”include 路径未配置检查附加包含目录是否包含 spdlog 的 include链接错误 LNK2019 / LNK2001使用编译库模式但未定义SPDLOG_COMPILED_LIB添加预处理定义或回到 header-only 模式CString传给 spdlog 报错字符集不匹配使用CT2A转成std::string后传入日志文件没有内容flush 策略未触发调用flush_on或flush_every或程序退出前shutdown()日志写入乱码文件编码与查看工具不匹配统一使用 UTF-8 编码或用 UTF-8 with BOM 写文件异步日志丢消息队列满了增大init_thread_pool队列长度或缩短业务侧日志频率多线程日志顺序错乱使用了_st单线程 logger改用_mt后缀的 logger日志文件被其他进程占用日志文件正被打开检查是否有其他程序占用文件调整文件访问模式控制台日志中文乱码Windows 控制台代码页问题执行chcp 65001或使用stdout_color_sink_mt设置 UTF-86.1 编译错误排查顺序如果在集成 spdlog 时遇到编译错误建议按以下顺序排查确认 spdlog 的 include 路径是否正确确认 C 标准是否为 C11 或更高确认是否是在 header-only 模式下使用了SPDLOG_COMPILED_LIB确认 Windows 项目中是否混用了/MD与/MT运行时库确认是否缺少必要的头文件比如文件 sink 需要单独 include。6.2 乱码问题详细分析在 MFC 项目中使用 spdlog 时乱码是最常见的坑。根因在于MFC 默认使用 Unicode 字符集spdlog 默认按char处理日志如果将wchar_t*直接转换成char*并写入文件如果未指定编码就可能与文件查看器预期不一致。推荐的统一做法是无论 MFC 工程是什么字符集进入 spdlog 之前统一转成 UTF-8 编码的std::string。Windows 下可以使用WideCharToMultiByte或CT2A。如果项目已经使用了 C17 的std::filesystem也可以用相关的转换工具但要注意代码的可移植性。#include windows.h #include string std::string WideToUtf8(const std::wstring wstr) { int size WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr); std::string str(size - 1, 0); WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, str[0], size, nullptr, nullptr); return str; }然后在 MFC 代码中调用CString msg; msg.Format(_T(用户 %s 登录失败), username); std::string utf8 WideToUtf8((LPCTSTR)msg); LOG_INFO({}, utf8);6.3 异步模式下的崩溃问题异步日志模式下如果程序在某个线程中写日志但同时其他线程正在调用spdlog::shutdown()可能会触发空指针或断言。解决方法是在所有业务线程退出后再调用shutdown()使用std::call_once保证shutdown()只调用一次在进程退出前显式spdlog::drop_all()清理 logger 资源。7. spdlog 最佳实践与工程建议spdlog 用起来很简单但要在真实项目中稳定运行需要遵循一些工程规范。7.1 日志级别使用规范日志级别的定义很容易被滥用。建议团队内部约定trace函数进入/退出、循环单次执行等极细粒度debug调试信息比如变量值、SQL 语句、HTTP 请求参数info关键业务节点比如用户登录、订单创建、服务启动warn可恢复异常、重试行为、性能下降等error业务失败、IO 异常、功能不可用critical进程无法继续运行、数据损坏等致命错误。线上环境建议将全局级别设为infodebug和trace通常留在本地调试使用。7.2 Pattern 格式统一日志格式需要统一规范便于后续采集分析。推荐的核心字段[时间] [级别] [线程ID] [文件名:行号] 消息内容具体 patternspdlog::set_pattern([%Y-%m-%d %H:%M:%S.%e] [%l] [%t] [%s:%#] %v);注意%s和%#需要源文件位置信息在某些编译器上需要开启对应选项也会带来少量性能开销建议只在 debug 或 warn 级别以上启用。7.3 文件大小与磁盘管理生产环境中一定要规划好日志增长策略单文件大小限制使用rotating_logger_mt按天归档使用daily_logger_mt定期清理历史日志或者结合系统日志轮转工具不要让日志文件无限制写入避免磁盘写满导致服务不可用。对于高并发服务建议将日志文件放在独立的磁盘分区避免日志 IO 与业务数据 IO 互相影响。7.4 安全与隐私日志中不要记录明文密码、令牌、身份证号、银行卡号等敏感信息。如果确实需要记录关联信息建议使用脱敏后的 ID 或哈希值。对日志文件本身也要设置合适的访问权限尤其是涉及用户数据时。7.5 性能优化建议优先使用异步 logger被高频调用的日志点先检查级别再拼字符串比如用if (logger-should_log(spdlog::level::debug))避免在日志参数中做复杂计算或 JSON 序列化避免在循环体内反复创建 logger程序退出时调用spdlog::shutdown()确保日志完整落盘。8. 小结spdlog 是 C 社区中成熟度非常高的一款日志库从控制台调试到服务器文件日志从同步写入到异步批量落盘它都提供了开箱即用的方案。对于普通业务开发者来说掌握 header-only 集成、基础 logger、文件 sink、异步日志、自定义格式就足够覆盖大多数场景对于 MFC 开发者来说重点在于 CString 转 UTF-8 和封装统一的日志管理类。接下来你可以继续探索spdlog 自带的sinks::syslog_sink在 Linux 服务中对接系统日志自定义 sink将日志转发到消息队列或远程日志系统结合 CMake 与第三方项目统一管理 spdlog 依赖。如果这篇文章对你有帮助可以先收藏备用。实际项目中引入 spdlog 后建议先写一个小型 Demo跑通编译、写入、轮转、异步刷新这几条链路再逐步接入业务代码。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门