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_LEVEL
是 SPDLOG_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. 异步记录器
在使用之前,请先了解异步记录器的短处:
- 异步记录时,日志记录的延迟更大(从日志产生到目标位置,异步记录包含更长的执行路径,包括队列操作、线程切换、线程间数据交换等费时操作);
- 如发生程序意外退出,丢失的日志可能更多(队列 + 系统缓冲区间的内容);
- 占用更多的内存(队列中积压的日志);
- 更复杂的参数优化调节:(后台线程池三大参数、运行环境性能配置、业务负载)。
异步记录这么多缺点?所以,通常我们就使用同步模式,但是,同步模式能满足性要求吗?什么情况下适合切换到异步?切换前后,需要注意哪些事项?
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 :不要轻易使用异步日志记录策略
不要轻易使用异步日志记录策略,因为它有很多短处:更长的记录路径,程序意外退时,会有更多的日志丢失,占用更多的内存,更复杂的调节工作……
解读:上不上异步日志记录?其实主要看你的月薪高低。