加载中...
spdlog-首选的C++日志库
第1节:libfswatch-文件变动监控
第2节:libiconv-字符集编码转换
第3节:CLI11-命令行参数解析
第4节:nlohmann/json-自然的JSON库
第5节:libb64-理解并玩转base64编码
第6节:libSnappy-快速压缩工具
第7节:spdlog-首选的C++日志库
课文封面
  • 六个小节
  • 六段视频
  • 十个练习
  • 十个项目最佳实践

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

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

spdlog 本身可直接使用头文件使用(相当于静态库,但每次编译会比较拖速度),也可使用安装时编译好的动态库(但视频中介绍到 SPDLOG_WCHAR_TO_UTF8_SUPPORT 无法对已经编译好的库发挥作用 )。

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

2. 独立使用日志记录器

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

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

3. 多个记录器,多个槽

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

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

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

4. 日志级别控制

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

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

  • 视频4: 日志级别控制

  • 各级日志的 WHAT 与 WHY
等级 偏向 WHAT WHY
trace 客观记录 记录基于实现的步骤信息(偏重记录正确路径) 展现代码的工作机制
debug 主观记录 只要有助于排查,记什么都行 排查特定BUG
info 客观记录 基于业务的过程信息 展现业务的日常运转过程
warn 主观记录 暂不影响系统运行的反常信息 提前发现问题
err 客观记录 出错信息 及时发现问题
critical 客观记录 会千万特定功能甚至系统整体失效的问题 补救
  • 静态控制
    一些实时(通常是近乎实时)的系统,比如各类证券交易系统,核心既必须记录日志,又对日志输出性能有着极为严苛的要求,这时可以考虑将代码中的全部或部分日志记录,改为静态模式。

比如,希望程序以 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: 日志格式控制

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

6. 异步记录器

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

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

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

7. 代码

  • 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)
  • 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()); } 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_异步日志记录器(); }

附:最佳实践解读

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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

日志级别理解

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


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

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

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


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

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

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