007-spdlog-日志库
一、简介、安装和测试
日志应用场景:
- 程序输出业务信息——最终用户;
- 输出运行信息——维护人员。
spdlog库的特点:将各种日志通过日志记录器(logger)吐到各种槽当中
- 标准输出槽、标准带色输出槽、标准错误输出槽;
- 单一文件槽、序号轮换文件槽、“日抛型”文件槽;
- 各系统的日志槽;
- 网络tcp/udp槽;
- 内存流槽;
- 其他自定义槽。
工程最佳实践1:日志仅包含程序运行的信息,不要包含多余的内容。
测试
创建cmake项目时需要注意包含以下3行:
set(CMAKE_EXE_LINKER_FLAGS "-static") #方便程序在外部控制台独立运行
target_link_libraries(${PROJECT_NAME} PRIVATE fmt) #spdlog无需预编译为库即可使用,但是其依赖fmt库
target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_WCHAR_TO_UTF8_SUPPORT) #输出到windows系统事件记录表中需用到汉字编码
cmake文件:
cmake_minimum_required( VERSION 3.10)
project(helloSpdlog VERSION 1.0 LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
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)
cpp文件:
#include <cstdlib>
#include <iostream>
#include <spdlog/spdlog.h>
int main()
{
#if defined(_WIN32)
std::system("chcp 65001 > nul");
#endif
spdlog::info("Welcome to spdlog version {}.{}.{} !", 1, 14, 1);
std::system("pause");
}
输出结果:
[2025-07-09 17:53:04.117] [info] Welcome to spdlog version 1.14.1 !
二、日志记录器logger
工程最佳实践2:优先使用全局记录器,实现统一的日志管理策略(简单-确定-统一)。
核心对象:logger
spdlog::default_logger():获取默认记录器;spdlog::set_default_logger(newLogger):替换默认记录器。
1.替换默认日志记录器

void spdlogColorlessOutput()
{
spdlog::info("这是一个信息日志");
spdlog::warn("这是一个警告日志");
auto colorless = spdlog::stdout_logger_mt("colorless");
spdlog::set_default_logger(colorless);
spdlog::info("这是一个信息日志");
spdlog::warn("这是一个警告日志");
}
输出:

变化:
- 输出的日志有了名字;
- 日志等级没有颜色。
2.记录器分类
2.1标准控制台(stdout)记录器
2.1.1不带色
头文件:<spdlog/sinks/stdout_sinks.h>
工厂方法:
spdlog::stdout_logger_mt(std::string const& logger_name);
spdlog::stdout_logger_st(std::string const& logger_name);
补充说明:为什么要输入logger的名字作为参数?
方便随时随地通过名字取得logger:
auto logger = spdlog::get("name")
2.1.2带色
头文件:<spdlog/sinks/stdout_color_sinks.h>
工厂方法:
spdlog::stdout_color_mt(std::string const& logger_name);
spdlog::stdout_color_st(std::string const& logger_name);
2.2错误控制台(stderr)记录器
// 不带色
spdlog::stderr_logger_mt(std::string const& logger_name);
spdlog::stderr_logger_st(std::string const& logger_name);
// 带色
spdlog::stderr_color_mt(std::string const& logger_name);
spdlog::stderr_color_st(std::string const& logger_name);
2.3文件记录器
2.3.1 basic_logger单一文件
// 工厂函数
basic_logger_mt(std::string const& logger_name,
std::string const& filename, //日志文件名
bool truncate = false //false:追加内容;true:清空文件内容然后写入
);
basic_logger_st(...);// 参数同上
2.3.2 rotating_file_sink回滚序号多文件
rotating_file_sink_mt(std::string const& logger_name,
std::string const& filename, //基础文件名
std::size_t max_size, //单个文件允许的最大字节数
std::size_t max_files, //最多文件个数
bool rotate_on_open = false, //首次打开立即滚号
std::string const& extension //文件拓展名
);
rotating_file_sink_mt(...);

2.3.3更多文件日志类型
daliy_logger_mt/st:按日分割hourly_logger_mt/st:按小时分割
工程最佳实践3:在Linux下,建议使用单一文件+追加策略,而将文件滚动拆分交给OS来实现——
- 实现更简单:仅需配置;
- 功能更强:如自动压缩;
- 更灵活:更换滚动策略而无需修改程序。
2.4文件记录器示例
void functionNeedFileLogger()
{
auto logger = spdlog::get("file_logger");
assert(logger);
logger->info("{} 函数中,通过名字为 {} 的记录器,输出本日志。", __FUNCTION__, logger->name());
}
void spdlogFileLogger()
{
spdlog::info("这是一个信息日志");
auto fileLogger = spdlog::basic_logger_mt("file_logger", "logs/log.txt");
fileLogger->info("这是一个信息日志");
fileLogger->warn("这是一个警告日志");
fileLogger->error("这是一个错误日志");
fileLogger->critical("这是一个关键日志");
functionNeedFileLogger();
}
日志信息:
[2025-07-09 22:12:06.852] [file_logger] [info] 这是一个信息日志
[2025-07-09 22:12:06.852] [file_logger] [warning] 这是一个警告日志
[2025-07-09 22:12:06.852] [file_logger] [error] 这是一个错误日志
[2025-07-09 22:12:06.852] [file_logger] [critical] 这是一个关键日志
[2025-07-09 22:12:06.852] [file_logger] [info] functionNeedFileLogger 函数中,通过名字为 file_logger 的记录器,输出本日志。
三、日志槽
1.一个记录器搭配多个槽

方法1:
- 创建多个槽;注:槽(sink)也用
shared_ptr来管理 - 【可选]】清空默认记录器原来的槽;
- 把创建的槽,加入(挂接)到默认记录器中。
方法2:
- 创建多个槽;
- 使用创建的槽,创建一个新的记录器;
- 【可选】使用新的记录器,替换全局默认记录器。
工程最佳实践4:在系统启动时,业务服务“窗口”未“开张”之前,在进程的主函数(通常就是在主函数开始),先初始化好日志记录器。换句话来说:不要在多线程语义环境内初始化日志记录器。
工程最佳实践5:
- 一个记录器同时挂接stdout和stderr两个槽;
- 让stdout输出普通信息+出错信息的日志;
- 让stderr仅输出出错日志(警告、错误、危急);
- 将stdout和stderr分别重定向到文件(因此会有两个日志文件,其中一个是完整日志,一个是仅含错误的日志)。
void spdlogRedirectOutput()
{
auto stdoutSink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto stderrSink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();
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("这是一个信息日志");
spdlog::warn("这是一个警告日志");
spdlog::error("这是一个错误日志");
spdlog::critical("这是一个关键日志");
}
输出结果:

接下来演示重定向:
在cmd中输入:.\helloSpdlog.exe 1>stdout.txt 2>stderr.txt
其中,“1”代表标准输出,“1>stdout.txt”表示将标准输出重定向到stdout.txt文件(即输出到该文件);“2”代表标准错误输出,表示将错误输出到stderr.txt文件中。
2.多个记录器,多个槽

工程最佳实践6:如何划分logger?
- 线程:每个工作线程使用一个日志记录器
- 业务:主业务、后台管理、统计业务、监控业务
- 层次:
- 业务层
- 网络层
- 设备层
- 错误做法:按业务流程步骤划分
工程最佳实践7:同一业务跨进程、跨主要实现时,应采用相同的日志记录策略,再通过后台脚本程序,实现合并。
实例:
创建一个项目,记录器与槽的关系如下:
- 默认(主业务)记录器 -> 带颜色的控制台槽 + 序号回滚文件槽
- 监控业务记录器 -> 普通控制台槽 + 单一文件槽 + windows系统事件槽
要点:
- 槽需要创建为智能指针
shared_ptr<sink>() - 记录器拥有一个
std::vector<T>动态数组,T就是智能指针 - spdlog没有为windows系统事件提供记录器工厂函数,需要先创建sink:
win_eventlog_sink_mt/st,再加入某个记录器
void multLoggerWithMultSink()
{
spdlog::info("全局日志记录器即将新增回滚编号文件槽");
// 1.全局日志记录器 - 颜色控制台槽 + 回滚编号文件槽
auto rotatingFileSink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"logs/rotatingLog.txt", 1024 * 1024 * 2,
4);
auto defaultLogger = spdlog::default_logger();
defaultLogger->sinks().push_back(rotatingFileSink);
spdlog::info("全局日志记录器已新增回滚编号文件槽");
for (int i = 0; i < 10000; ++i) {
spdlog::info("全局日志记录器输出第{}条日志。", i + 1);
spdlog::warn("全局日志记录器输出第{}条日志。", i + 1);
spdlog::error("全局日志记录器输出第{}条日志。", i + 1);
}
// 2.专门用于监控业务的日志记录器
auto colorlessOutSink = std::make_shared<spdlog::sinks::stdout_sink_mt>();
// 创建普通文件槽
auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/monitorLog.txt");
// 创建windows OS下使用的槽
#if _WIN32
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(colorlessOutSink);
monitorLogger->sinks().push_back(fileSink);
#if _WIN32
monitorLogger->sinks().push_back(winEvtSink);
#endif
monitorLogger->info("监控日志槽已创建,他有{}个槽。", monitorLogger->sinks().size());
for (int i = 0; i < 100; ++i) {
monitorLogger->info("监控日志记录器输出第{}条日志。", i + 1);
monitorLogger->warn("监控日志记录器输出第{}条日志。", i + 1);
monitorLogger->error("监控日志记录器输出第{}条日志。", i + 1);
}
}
效果:
仅展示windows日志系统的效果:
右键单击win,选择事件查看器,在应用程序日志中,可以看到日志信息:

四、日志级别控制
工程最佳实践8:如何确认一条日志的级别?
主观记录:注重为社么
客观记录:注重记什么
1.trace 跟踪
- 记什么:记录基于(技术)实现的步骤信息(通常偏重记录正确路径)
- 为社么:明确当前代码的工作机制
2.debug 调试
- 为什么:找到bug
- 记什么:任何可能导致bug的原因
- 注:当前代码的bug完全解决后,删除debug信息
3.info 信息
- 记什么:记录基于业务的过程信息(里程碑、关键节点、状态)
- 为什么:帮助系统发现运行状态是正常的
4.warn 警告
- 为什么:系统的每一笔业务都能正常的执行,但是出现了反常信息
- 记什么:不影响系统业务进行的每一笔反常信息
5.error 错误
- 为什么:某一笔业务出错了
- 记什么:业务执行出错的原因
6.critical 危急
- 记什么:能够让系统某个功能直接罢工的事件
- 为什么:错误危害到了系统的正常运行
7.spdlog的级别控制
采用串联过滤:记录器过滤掉低级别的日志后,传递给后续的槽,再由槽进行二次过滤

新建的槽默认使用最低级别trace,所有的记录器和槽都调用 set_level函数设置日志级别,如果需要完全关闭日志,则调用:
logger->set_level(level::off);
同时:
level::to_string_view():得到级别枚举值名称level::to_short_c_str():得到大写单字母名称
工程最佳实践9:
- 将槽加入到记录器之前,先设置它的级别;
- 用常量定义sink的加入顺序,后续如确实需要调整级别,使用该常量下标;
- sink加入logger后,如需关闭其输出,使用set_level(off),而不是删除sink。
示例:
void setLevelTest()
{
int const IDX_CONSOLE_SINK = 0;
int const IDX_FILE_SINK = 1;
auto levelLogger = spdlog::stdout_color_mt("LevelLogger");
// 创建文件槽
auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/levelTestLog.txt");
// 设置日志级别
fileSink->set_level(spdlog::level::warn);
// 加入到记录器
levelLogger->sinks().push_back(fileSink);
// 查看记录器的级别
levelLogger->info("记录器级别为:{}", spdlog::level::to_string_view(levelLogger->level()));
// 查看各个槽的级别
for (auto sink : levelLogger->sinks()) {
levelLogger->info("槽的级别为:{}", spdlog::level::to_string_view(sink->level()));
}
levelLogger->info("记录器输出一条信息日志");
levelLogger->debug("记录器输出一条调试日志");
// 调整记录器级别
levelLogger->set_level(spdlog::level::debug);
levelLogger->debug("记录器的级别已经调整到了 debug");
levelLogger->warn("不过 debug 和 info 级别的日志依然不会输出到文件");
levelLogger->sinks()[IDX_FILE_SINK]->set_level(spdlog::level::debug);
levelLogger->debug("文件槽的级别也被调整为了 debug");
}
8.日志级别的静态控制
一些实时(通常是近乎实时)的系统,比如各类证券交易系统,核心既必须记录日志,又对日志输出性能有着极为严苛的要求,这时可以考虑将代码中的全部或部分日志记录,改为静态模式。
比如,希望程序以 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),然后重新编译生成程序。
注意,静态和动态可以混合使用,你可以只在性能超级敏感的模块中,使用静态日志输出。
五、日志格式控制
严格来说,仅是“模式匹配”
1.使用 {}作为占位符
2.自定义输出内容匹配模式(pattern)
logger/sink->set_pattern("格式指定串")
例如:
logger->set_pattern("[%Y年%m月%d日 %H:%M:%S]- 【%l】 %n::%^%v%$");
%Y-%m-%d %H:%M:%S:表示日期和时间;%l:表示日志级别;%^%$:用于设置颜色作用在二者之间的内容,仅对控制台有效;%n:记录器名称;%v:原始日志内容。
void setSpdlogFormat()
{
spdlog::info("原有格式");
spdlog::info("服务已在 {}:{} 开始监听", "127.0.0.1", 8080);
auto mainLogger = spdlog::stdout_color_mt("mainLogger");
mainLogger->set_pattern("[%Y年-%m月-%d日 %H:%M:%S.%e] [%^%l%$] %v");
auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/formatTestLog.txt");
fileSink->set_pattern("[%Y年-%m月-%d日 %H:%M:%S.%e] [%^%l%$] %v");
mainLogger->sinks().push_back(fileSink);
mainLogger->set_level(spdlog::level::trace);
spdlog::set_default_logger(mainLogger);
spdlog::info("修改后的格式");
spdlog::info("服务已在 {}:{} 开始监听", "127.0.0.1", 8080);
spdlog::warn("服务已在 {}:{} 开始监听");
spdlog::debug("服务已在 {}:{} 开始监听");
spdlog::critical("服务已在 {}:{} 开始监听");
spdlog::error("服务已在 {}:{} 开始监听");
}
效果:

六、异步记录器
1.同步记录器

- 同步≠立即写入:虽然是同步模式,在目标有缓冲区,且数据未灌满缓冲区时,日志并未输出到指定位置,仍有可能丢失;
- 后续步骤需要等待:同步模式下,必须等到写日志完事后,才能继续后续语句。
为什么不立即写入?

绝大多数情况下,同步写入就足够满足要求。
补丁:
- 对特定步骤下的关键日志借助
flush()强制输出; - 启用
spdlog::flush_every(时长),定期强制输出。
void spdlogBufferFlush()
{
auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/bufferFlushLog.txt");
spdlog::default_logger()->sinks().push_back(fileSink);
spdlog::info("服务已在 {}:{} 开始监听", "127.0.0.1", 8080);
std::system("pause");
spdlog::default_logger()->flush();
spdlog::info("服务已在 {}:{} 开始监听", "127.0.0.1", 8080);
}
2.异步记录器


相比于同步,异步记录的劣势:
- 日志记录的延迟更大(更长的路径,包括线程切换);
- 如发生程序意外退出,丢失的日志内容会更多;
- 占用更多的内存;
- 更复杂的参数调节。
优势:
- 大压力下,对业务线程(日志生产者)的负面影响更低
工程最佳实践10:
- 优先使用同步日志记录器。当实际日志量超过1w条/秒时,再考虑使用异步记录模式,并通过数据对比;
- 主要对比数据:1.业务自身的处理能力能否得到提升?2.每秒日志记录量是否显著提升?
- 异步参数调参依据:业务线程cpu占用率是否下降?积压日志的任务是否容易超限?进程内存是否吃得住?

