加载中...
spdlog-首选的C++日志库
第1节:libfswatch-文件变动监控
第2节:libiconv-字符集编码转换
第3节:CLI11-命令行参数解析
第4节:nlohmann/json-自然的JSON库
第5节:libb64-理解并玩转base64编码
第6节:libSnappy-快速压缩工具
第7节:spdlog-首选的C++日志库
第8节:libUSB-脱掉USB的外套
第9节:libxlsxwriter-让数据说话
第10节:sqlite-orm-迅速上手.上
第11节:sqlite-orm-迅速上手.下
第12节:libExiv2-读写图像元数据
第13节:webview-让浏览器颤抖的力量
第14节:pystring-躺平的C++字符串工具库
第15节:cpr-自制Deepseek心理陪聊师
第16节:ExprTk-计算的力量
课文封面
  • 六个小节
  • 六段视频
  • 十个练习
  • 十个项目最佳实践

从入门到精通,一节课搞定 C++ 日志编程

1. 简介、安装、测试

1.1 安装

Windows msys2 下安装( 以 urct64 环境为例):

pacman -S mingw-w64-ucrt-x86_64-spdlog

UBUNTU Linux:

sudo apt install libspdlog-dev

苹果 MacOS

Homebrew:brew install spdlog 或: MacPorts:sudo port install spdlog

1.2 使用方式

直接在目标项目的源代码,#include <spdlog/spdlog.h> 等头文件,即可正常使用——不过记得它可能依赖 fmt 库——相当方便,缺点是严重拉低编译速度。幸好,前面使用 msys2 安装库时,也会得到已经编译好的 spdlog 动态库。

使用 msys2 现成的库,也有缺点:

  • 只有动态库,如希达成静态库效果,请退回使用纯头文件方式;
  • msys2 所编译的 spdlog,没有添加后续视频所提的 SPDLOG_WCHAR_TO_UTF8_SUPPORT 宏,因此库中没有宽字符与UTF8字符的转换功能,我们的项目再定义该宏,也无济于事。

本课视频演示中,均使用纯头文件方式。不过,在第 7 小节代码中,提供了使用库的 CMakeLists.txt。

1.3 视频1: spdlog 简介、安装与试用

2. 独立使用日志记录器

spdlog 库中最重要的两个概念:日志记录器(logger,也称日志对象) 和 槽(sink)。日常输出日志,主要通过 logger 来实现。

视频2: spdlog 独立使用日志记录器(logger)

3. 多个记录器,多个槽

将一行日志,同时输出到多个目标位置(屏幕、文件、网络等),是日志编程的最佳实践,在 spdlog 中,该功能被实现为:一个日志记录器(logger),可以挂接多个槽(sinks)。

那么,同一个程序中,什么时候需要使用多个日志记录器呢?

视频3: 多个记录器,多个槽

4. 日志级别控制

记住:最重要的控制,来自对程序员的控制,因为,代码中所有日志,都是程序员写的。源头没控制好,后面再多技术手段也救不了。

首先要理解什么叫日志的 “主观记录” ,什么叫 “客观记录”。

4.1 视频4: 日志级别控制

4.2 各级日志的 WHAT 与 WHY

等级 偏向 WHAT WHY
trace 客观记录 记录基于实现的步骤信息(偏重记录正确路径) 展现代码的工作机制
debug 主观记录 只要有助于排查,记什么都行 排查特定BUG
info 客观记录 基于业务的过程信息 展现业务的日常运转过程
warn 主观记录 暂不影响系统运行的反常信息 提前发现问题
err 客观记录 出错信息 及时发现问题
critical 客观记录 会千万特定功能甚至系统整体失效的问题 补救

4.3 级别的静态控制

一些实时(通常是近乎实时)的系统,比如各类证券交易系统,核心既必须记录日志,又对日志输出性能有着极为严苛的要求,这时可以考虑将代码中的全部或部分日志记录,改为静态模式。

比如,希望程序以 INFO 级别记录日志,可以先在项目中全局定义以下宏的值:

#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO

然后,将记录日志的代码,由原来的 spdlog::info( ... )spdlog::err( ... ),改成如下:

auto logger = spdlog::default_logger(); SPDLOG_LOGGER_TRACE(logger, "这是一条跟踪日志"); //1 SPDLOG_LOGGER_DEBUG(logger, "这是一条调试日志"); //2 SPDLOG_LOGGER_INFO(logger, "这是一条信息日志"); //3 SPDLOG_LOGGER_WARN(logger, "这是一条警告日志"); //4 SPDLOG_LOGGER_ERROR(logger, "这是一条出错日志"); //5 SPDLOG_LOGGER_CRITICAL(logger, "这是一条危急日志"); //6

因为此时我们设置的 SPDLOG_ACTIVE_LEVELSPDLOG_LEVEL_INFO,所以,上面代码中,比如 INFO 级别低的 1、2 两行代码(宏),会在编译期间就直接被无视(抛弃),仿佛它们从来没有来到过这人间……

这样做的好处,就是可以保证:不需要输出的日志操作,在代码中完全不存在,从而将它们对性能的负面影响,完全消除。

这样的坏处,也很明显:当你需要调节日志输出级别,你的唯一办法是:修改全局宏 SPDLOG_ACTIVE_LEVEL 的值(SPDLOG_LEVEL_XXX),然后重新编译生成程序。

注意,静态和动态可以混合使用,你可以只在性能超级敏感的模块中,使用静态日志输出。

5. 日志格式控制

想对日志输出内容与格式做完全控制(或彻底的定制),需派生 spdlog 的 format 接口,不过,多数情况下,我们就是使用 spdlog 默认提供的内容与格式。默认日志记录器输出的内容,长这样子:

[2025-06-24 00:07:32.644] [info] 服务器已在 36.251.248.218 : 80 开始监听

包含时间、等级等。偶尔想调整,只需使用日志记录器或槽的 set_pattern() 方法即可。

5.1 视频5: 日志格式控制

5.2 常用格式控制串

- %Y-%m-%d %H:%M:%S:表示日期和时间。​
- %l:%l表示日志级别
- %^%$:用于设置颜色作用在二者之间的内容,仅对控制台有效,截止1.15.2 版本,只能用一次
- %n:记录器名称(如前所述,通常取业务或层次名称)
- %v:原始日志内容

6. 异步记录器

在使用之前,请先了解异步记录器的短处:

  1. 异步记录时,日志记录的延迟更大(从日志产生到目标位置,异步记录包含更长的执行路径,包括队列操作、线程切换、线程间数据交换等费时操作);
  2. 如发生程序意外退出,丢失的日志可能更多(队列 + 系统缓冲区间的内容);
  3. 占用更多的内存(队列中积压的日志);
  4. 更复杂的参数优化调节:(后台线程池三大参数、运行环境性能配置、业务负载)。

异步记录这么多缺点?所以,通常我们就使用同步模式,但是,同步模式能满足性要求吗?什么情况下适合切换到异步?切换前后,需要注意哪些事项?

视频6: 异步日志记录

7. 代码

7.1 仅使用头文件

  • CMakeLists.txt
cmake_minimum_required(VERSION 3.10) project(helloSpdlog VERSION 0.1.0 LANGUAGES CXX C) set(CMAKE_CXX_STANDARD 17) set(CMAKE_EXE_LINKER_FLAGS "-static") add_executable(helloSpdlog main.cpp) target_link_libraries(${PROJECT_NAME} PRIVATE fmt) target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_WCHAR_TO_UTF8_SUPPORT)

7.2 使用 spdlog 动态库

在真实的中大型项目中,通常希望缩短编译时长,此时可考虑改为使用已编译的 spdlog 库,不过,msys2 只提供了动态版本的 spdlog 库(扩展名为 .dll),因此项目无法再使用静态方式(即:spdlog 和 其所依赖的 fmt 库,以及 msys2-ucrt64 环境所需的库,都将改为使用动态库)。

下面是默认使用 spdlog 动态库的 CMakeLists.txt ,并且支持切换:将 USE_COMPILED_SPDLOG_LIB 切换为 OFF,即恢复为使用 spdlog 头文件加静态链接 fmt 等库的构建方式)。

  • CMakeLists.txt
cmake_minimum_required(VERSION 3.10) project(helloSpdlog VERSION 0.1.0 LANGUAGES CXX C) set(CMAKE_CXX_STANDARD 17) # 如果需要使用纯头文件形,将 ON 改为 OFF set(USE_COMPILED_SPDLOG_LIB ON) add_executable(helloSpdlog main.cpp) if (USE_COMPILED_SPDLOG_LIB) message(STATUS "Using compiled spdlog library") # 需要通过定义 SPDLOG_COMPILED_LIB , 另 msys2 只提供动态链接库, # 因此,还需要定义 SPDLOG_SHARED_LIB target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_COMPILED_LIB) target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_SHARED_LIB) target_link_libraries(${PROJECT_NAME} PRIVATE spdlog) # 如果你的 cmake 并不是 msys2 提供的,你还得告诉它到哪找 spdlog (以及 fmt) 库 target_link_directories(${PROJECT_NAME} PRIVATE "C:/msys64/ucrt64/bin") # 以及,上哪找 spdlog (以及 fmt) 的头文件 target_include_directories(${PROJECT_NAME} PRIVATE "C:/msys64/ucrt64/include") else() set(CMAKE_EXE_LINKER_FLAGS "-static") endif() # 不管 是否 USE_COMPILED_SPDLOG_LIB,fmt 总是必须的 target_link_libraries(${PROJECT_NAME} PRIVATE fmt) # 让 spdlog 支持 输出 wchar_t # 如果使用 msys2 已编译的 spdlog ,此宏无效,因为 msys2 就没用它 if (NOT USE_COMPILED_SPDLOG_LIB) target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_WCHAR_TO_UTF8_SUPPORT) endif()

7.3 main.cpp

#include <cassert> #include <cstdlib> #include <iostream> #include <chrono> #include <spdlog/spdlog.h> #include <spdlog/sinks/stdout_sinks.h> #include <spdlog/sinks/basic_file_sink.h> #include <spdlog/sinks/stdout_color_sinks.h> #include <spdlog/sinks/rotating_file_sink.h> #include <spdlog/sinks/win_eventlog_sink.h> #include <spdlog/async.h> void main1_HelloSpdLog() { char const* lib7 = "spdlog"; spdlog::info("Hello {} !", lib7); // Hello spdlog! } void main2_替换全局日志记录器() { spdlog::info("替换之前,等级显示是带颜色的!"); spdlog::warn("这是警告!,颜色应该更‘娇艳’!"); // 创建新的记录器(不带颜色的标准输出) auto colorlessLogger = spdlog::stdout_logger_mt("Colorless"); spdlog::set_default_logger(colorlessLogger); // 用新的全局记录器输出: spdlog::info("替换之后,等级显示不带颜色!"); spdlog::warn("这是警告!,一切都很清心寡欲。"); } void functionNeedFileLogger() { auto logger = spdlog::get("FileLogger"); assert(logger); logger->info("{} 函数中,通过名字是 {} 的记录器,输出本日志", __FUNCTION__, logger->name()); } void main3_使用文件日志记录器() { spdlog::info("下面有些内容,我们只输出到日志文件"); auto fileLogger = spdlog::basic_logger_mt("FileLogger", "log/file-log.txt"); fileLogger->warn("完蛋,用户太帅了,我有心动的感觉!"); spdlog::error("无法识别人脸,用户,请正面面对摄像头!"); functionNeedFileLogger(); } void main4_重定向标准输出() { // 创建标准输出(带彩色)槽 auto stdoutSink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); // 创建一个标准错误输出(带彩色)槽 auto stderrSink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>(); // 设置 stderr 槽只输出警告及以上级别的日志(建议:在挂接之前设置好) stderrSink->set_level(spdlog::level::warn); // 取得默认的日志记录器,并清空它原有的槽 auto defaultLogger = spdlog::default_logger(); defaultLogger->sinks().clear(); // 将上面的新创建的槽挂接到默认记录器: defaultLogger->sinks().push_back(stdoutSink); defaultLogger->sinks().push_back(stderrSink); spdlog::info("1、我们一定要团结一致!"); spdlog::error("2、公司人事真是大聪明!连续安排程序员三个周末都加班?!"); spdlog::info("3、今天天气,哈哈哈~"); spdlog::critical("4、今天老板娘和老板在办公室打起来了!"); } void main5_多个记录器() { spdlog::info("全局日志记录器即将新增回滚编号文件槽"); // 1. 全局日志记录器 - 颜色控制台槽 + 回滚编号文件槽 auto rotatingFileSink = std::make_shared<spdlog::sinks ::rotating_file_sink_mt>("log/main-rotating.txt" , 1024 * 1024 * 5, 9); // 取默认记录器 auto defaultLogger = spdlog::default_logger(); defaultLogger->sinks().push_back(rotatingFileSink); // 加入新槽 spdlog::info("全局日志记录器已添加回滚编号文件槽"); // 2. 专用于监控业务的日志记录器 auto colornessOutSink = std::make_shared<spdlog::sinks::stdout_sink_mt>(); // 创建普通文件槽 auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("log/monitor.txt"); #ifdef _WIN32 // 创建 Windows OS 的事件记录槽 auto winEvtSink = std::make_shared<spdlog::sinks::win_eventlog_sink_mt>("HelloSpdlog"); #endif // 创建一个创新的日志记录器,注意取名要能体现它所服务的业务 auto monitorLogger = std::make_shared<spdlog::logger>("MonitorLogger"); monitorLogger->sinks().push_back(colornessOutSink); monitorLogger->sinks().push_back(fileSink); #ifdef _WIN32 monitorLogger->sinks().push_back(winEvtSink); #endif monitorLogger->info("监控日志记录器已经就绪!它有 {} 个槽", monitorLogger->sinks().size()); } void main6_日志级别调整() { int const IDX_CONSOLE_SINK = 0; // 控制台槽的下标 int const IDX_FILE_SINK = 1; // 文件槽的下标 auto levelsLogger = spdlog::stdout_color_mt("LevelsLogger"); // 创建文件槽 auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("log/levels.txt"); // 修改文件槽的级别: fileSink->set_level(spdlog::level::warn); // 加入到记录器 levelsLogger->sinks().push_back(fileSink); // 查看记录器的级别: levelsLogger->info("LevelsLogger 记录器的级别:{} ", , spdlog::level::to_string_view(levelsLogger->level())); // 查看各个槽的级别: levelsLogger->info("各个槽的级别"); for (auto sink : levelsLogger->sinks()) { levelsLogger->info(spdlog::level::to_short_c_str(sink->level())); } levelsLogger->info("这行日志只会在屏幕显示"); levelsLogger->debug("这行调试日志,在屏幕和文件都不会显示"); // 先调整记录器的级别 到 debug levelsLogger->set_level(spdlog::level::debug); levelsLogger->debug("本记录器等级已调整为 debug"); levelsLogger->warn("不过,debug 和 info 级别的日志仍然不会输出到文件"); levelsLogger->sinks()[IDX_FILE_SINK]->set_level(spdlog::level::debug); levelsLogger->debug("文件槽的级别也已调整为 debug!"); } void main7_修改Pattern() { spdlog::info("原有格式"); spdlog::info("服务已经在 {} : {} 开始监听", "36.251.248.218", 8090); spdlog::info("开始修改 Pattern"); // 先创建一个带颜色的控制台日志记录器 auto mainLogger = spdlog::stdout_color_mt("主站"); // 修改它的 Pattern mainLogger->set_pattern("[%Y年%m月%d日 %H:%M:%S]-%^〚%l〛%n::%v%$"); // 创建一个文件 sink auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>( "log/pattern.txt"); fileSink->set_pattern("[%Y-%m-%d %H:%M:%S] >%l< [%n] %v"); mainLogger->sinks().push_back(fileSink); // 调整为最低级别 mainLogger->set_level(spdlog::level::trace); // 取代默认 spdlog::set_default_logger(mainLogger); spdlog::info("自定义格式起作用了!"); spdlog::info("服务已经在 {} : {} 开始监听", "36.251.248.218", 8090); spdlog::warn("服务器感觉有点卡卡的"); spdlog::error("服务器无法连接数据库了!"); spdlog::critical("糟糕,好像机房着火了!!!"); spdlog::trace("再见,我先走了"); } void main8_flush缓冲区() { auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("log/flush.txt"); spdlog::default_logger()->sinks().push_back(fileSink); spdlog::info("写入日志,请对比屏幕输出和 flush.txt 的内容"); std::system("pause"); spdlog::default_logger()->flush(); spdlog::info("已经强制刷新日志缓冲区,请重新观察"); std::system("pause"); } void main9_flush_every缓冲区() { // 每三秒强制清空缓冲区一次 spdlog::flush_every(std::chrono::seconds(3)); auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("log/flush_every.txt"); spdlog::default_logger()->sinks().push_back(fileSink); spdlog::info("写入日志,请打开 flush_every.txt 并静待 3 秒"); spdlog::info("写入日志,请打开 flush_every.txt 并静待 2 秒"); spdlog::info("写入日志,请打开 flush_every.txt 并静待 1 秒"); std::system("pause"); } void main10_异步日志记录器() { spdlog::init_thread_pool(1025 * 10, 1); // 为异步日志记录器的工厂类型,取简短的别名: /* using async_factory = spdlog::async_factory_impl<spdlog::async_overflow_policy::block>; using async_factory_nb = spdlog::async_factory_impl<spdlog::async_overflow_policy::overrun_oldest>;*/ // 创建带颜色的控制台日志记录器-使用指定异步工厂 // 非堵塞:async_factory_nonblock auto asyncColorLogger = spdlog::stdout_color_mt<spdlog::async_factory>("AsyncLogger"); ////// 下面的操作(记录日志),就跟同步的日志记录器完全一样了 /////// // 创建文件 sink auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>( "log/async-file.txt"); asyncColorLogger->sinks().push_back(fileSink); // 替换默认: spdlog::set_default_logger(asyncColorLogger); spdlog::info("异步日志记录器已经就绪!,它有 {} 个 槽" , spdlog::default_logger()->sinks().size()); std::system("pause"); } int main() { #ifdef _WIN32 std::system("chcp 65001 > nul"); #endif main1_HelloSpdLog(); main2_替换全局日志记录器(); main3_使用文件日志记录器(); main4_重定向标准输出(); main5_多个记录器(); main6_日志级别调整(); main7_修改Pattern(); main8_flush缓冲区(); main9_flush_every缓冲区(); main10_异步日志记录器(); spdlog::shutdown(); // 使用现成的库时,建议退出前主动关闭 }

附:最佳实践解读

  • 工程最佳实践-1 :日志就是日志

日志就是日志,请就用来记录程序的运行信息,不要夹杂其他内容。特别地,不要在里面写情诗,更不要在里面疯狂地吐槽客户或老板。

解读:做一个情绪稳定的程序员。


  • 工程最佳实践-2 :优先使用全局记录器

优先使用全局记录器,因为它最简单,最有确定性,最重要的是:同一程序的日志记录格式、策略等,应该尽量保持一致,避免让系统维护者需要面对 “五花八门” 的日志内容。

解读:把最简单的使用方式,留给最广泛的使用需要——这是好的设计。


  • 工程最佳实践-3 :放手让 OS 处理日志文件

放手让 OS 处理日志文件,典型如 Linux/unix/mac 系统,有成熟的文件自动分割(支持策略的丰富性,远高于任何一个日志程序库)、压缩、备份的工具。

解读:不要试图用自己的代码包办一切,专业的事情交给专业的工具做。


  • 工程最佳实践-4 :在业务开张之前,初始化好你的日志对象

在业务开张之前,初始化好你的日志对,在进入 main()后,程序的业务功能还未开展之前,就初始化和日志相关的一切工具。特别地,不要莫名其妙地让自己陷入要在 “多线程” 环境下如何并发安全地初始化全局对象的困境……

解读:不要抵抗这条原则,除非你就喜欢一边穿裤子一边冲入地铁站。


  • 工程最佳实践-5 :用好重定向,鱼与熊掌可得兼

用好重定向,鱼与熊掌可得兼,“鱼” :程序日志可以输出到屏幕(不重定向),“熊掌” :也可以输出到文件(重定向);“鱼”:独立的出错日志,避免问题被埋没;“熊掌”:完整时间线上的各级日志,方便分析问题的产生原因。

解读:善用工具,简单和丰富可得兼,高薪和秀发都不失


  • 工程最佳实践-6 :合理划分你的日志记录器

合理划分你的日志记录器,当然,如前所述:最合理的划分,就是不划分。不过,如果软件系统现有架构确实已经相对复杂,那么,你可以按线程划分,如果你的系统有稳定的业务线程(通常以给线程池的形式存在),且使用业务流程与单一线程绑定的逻辑。也可以为不同的业务分配不同的日志记录器,或者,按系统的依赖与支撑分层,分配各自的日志记录器。

解读:学会从架构层面思考问题。


  • 工程最佳实践-7 :使用简单的语言,合并跨进程的日志

使用简单的语言,合并跨进程的日志,无可避免,随着系统的复杂化,一笔完整业务的流程,最终分被折分到多个程序甚至多台机器上,这时应考虑学会使用一些简单的语言(Python、Go )写工具,完成日志在时间线上的合并——顺便说一下,如果跨主机的话,记得配置好不同主机上的时间同步。

解读:高手为什么会懂那么多门编程语言?


  • 工程最佳实践-8 :在源头上控制好日志级别

在源头上控制好日志级别,这个源头就是人,就是程序员,建议按下图的内容统一“洗脑”。

日志级别理解

解读:管不好人,领导不了技术。


  • 工程最佳实践-9 :定下来的日志设置,就不要改了

定下来的日志设置,就不要改了,非要在程序运行期间动态修改,那就只改它们的日志控制级别吧。

解读:当产品经理或客户要求程序运行时,日志格式,记录策略要随时能改……让他们来找南老师。


  • 工程最佳实践-10 :不要轻易使用异步日志记录策略

不要轻易使用异步日志记录策略,因为它有很多短处:更长的记录路径,程序意外退时,会有更多的日志丢失,占用更多的内存,更复杂的调节工作……

解读:上不上异步日志记录?其实主要看你的月薪高低。