第63章 spdlog:集成spdlog日志库
日志这东西,说白了就是程序的「黑匣子」。我刚开始写C++那会儿,习惯用printf或者cout来打日志,结果项目一大了,满屏的调试信息根本分不清哪些是错误、哪些是警告。后来我接手一个线上服务,崩溃了连个堆栈都找不到,那叫一个痛苦。嗯,从那以后,我就老老实实把日志库集成到项目里了。
今天要聊的spdlog,是我个人最常用的C++日志库。它快、头文件少、配置灵活,而且对CMake的支持非常友好。你想想看,一个日志库如果集成起来要折腾半天,那谁还愿意用?
为什么选spdlog?
市面上C++日志库不少,比如glog、log4cxx、Boost.Log。但我为什么偏爱spdlog?
- 纯头文件模式:大部分功能只需要include头文件,不需要编译库文件
- 性能极好:异步日志模式下,写日志几乎不阻塞业务线程
- 格式化灵活:支持类似fmt的格式化语法,比流式输出清爽多了
- 多输出目标:控制台、文件、syslog、甚至自定义sink
- CMake集成简单:fetch、find、add_subdirectory都行
我在项目中遇到过几次日志库拖慢性能的情况,后来换成spdlog的异步模式,问题就解决了。说白了,日志库不能成为性能瓶颈。
CMake集成方式
spdlog的集成方式有好几种,我按推荐程度排个序:
| 方式 | 命令 | 适用场景 |
|---|---|---|
| FetchContent | FetchContent_Declare(spdlog ...) | 网络畅通,想要最新版本 |
| find_package | find_package(spdlog REQUIRED) | 系统已安装spdlog |
| add_subdirectory | add_subdirectory(third_party/spdlog) | 项目自带源码,离线环境 |
| vcpkg/conan | 包管理器安装 | 团队统一管理依赖 |
我个人习惯用FetchContent,因为它不需要预先安装,而且版本控制方便。下面是一个完整的CMakeLists.txt示例:
cmake_minimum_required(VERSION 3.14)
project(MyApp LANGUAGES CXX)
# 启用C++17(spdlog需要)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 使用FetchContent拉取spdlog
include(FetchContent)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.12.0
)
FetchContent_MakeAvailable(spdlog)
# 生成可执行文件
add_executable(my_app main.cpp)
# 链接spdlog
target_link_libraries(my_app PRIVATE spdlog::spdlog)
# 如果只需要头文件模式,可以这样:
# target_link_libraries(my_app PRIVATE spdlog::spdlog_header_only)
这里有个小细节:spdlog默认是编译成静态库的,如果你只想用头文件模式,链接spdlog::spdlog_header_only就行。不过我个人建议用编译模式,编译速度会快一些。
代码中使用spdlog
集成完之后,写日志就非常简单了。看个例子:
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
int main() {
// 控制台日志
spdlog::info("欢迎使用 spdlog!");
spdlog::warn("这是一条警告信息");
spdlog::error("这是一条错误信息");
// 带格式的日志
spdlog::info("用户 {} 登录成功,ID: {}", "张三", 10086);
// 创建文件日志
auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/app.log");
file_logger->info("这条日志会写入文件");
// 设置日志级别
spdlog::set_level(spdlog::level::debug);
spdlog::debug("这条debug信息只有在级别设置为debug时才会显示");
return 0;
}
你看,用起来跟printf差不多,但多了日志级别、文件输出、格式化等功能。而且spdlog的格式化语法跟C++20的std::format很像,如果你用过Python的f-string,上手会非常快。
异步日志配置
我曾经遇到过一个场景:业务线程每秒要处理上万条请求,每条请求都要写日志。如果同步写文件,性能直接崩了。这时候就需要异步日志:
#include <spdlog/async.h>
#include <spdlog/sinks/rotating_file_sink.h>
int main() {
// 设置异步日志的队列大小和线程数
spdlog::init_thread_pool(8192, 1);
// 创建异步文件日志(带轮转)
auto async_file = spdlog::rotating_logger_mt<spdlog::async_factory>(
"async_logger", "logs/async.log", 1024 * 1024 * 5, 3);
async_file->info("这是异步日志,不会阻塞主线程");
// 也可以设置刷新策略
async_file->flush_on(spdlog::level::warn);
return 0;
}
这里要注意:rotating_logger_mt是线程安全的版本,如果你的程序是单线程的,可以用_st版本,性能会更好一点。
自定义日志格式
默认的日志格式是[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v,也就是「时间戳 + 日志级别 + 消息」。但有时候我们需要自定义格式,比如加上线程ID、源文件名等:
#include <spdlog/spdlog.h>
int main() {
// 自定义格式:时间 [线程ID] 级别 文件:行号 - 消息
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%t] [%^%l%$] %s:%# - %v");
spdlog::info("这条日志会显示文件名和行号");
// 也可以为特定logger设置格式
auto logger = spdlog::stdout_color_mt("console");
logger->set_pattern("%^[%l]%$ %v");
return 0;
}
格式说明符有很多,我列几个常用的:
| 说明符 | 含义 |
|---|---|
| %v | 日志消息内容 |
| %l | 日志级别(小写) |
| %L | 日志级别(大写) |
| %t | 线程ID |
| %s | 源文件名 |
| %# | 源文件行号 |
| %! | 函数名 |
| %^ / %$ | 颜色范围开始/结束 |
多Sink组合
有时候我们需要同时输出到控制台和文件,而且不同sink可以有不同的日志级别。比如控制台只显示警告以上,文件记录所有信息:
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
int main() {
// 创建两个sink
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/multi.log");
// 设置不同级别
console_sink->set_level(spdlog::level::warn);
file_sink->set_level(spdlog::level::trace);
// 组合成logger
spdlog::logger logger("multi_sink", {console_sink, file_sink});
logger.set_level(spdlog::level::trace);
logger.trace("这条只写入文件");
logger.info("这条也只写入文件");
logger.warn("控制台和文件都会显示");
logger.error("同样两边都显示");
return 0;
}
这个模式我在微服务项目里经常用:控制台只看错误,文件保留全量日志用于排查问题。
避坑指南
- 忘记flush:程序崩溃时,最后几条日志可能没写入文件。建议设置
flush_on为warn或err级别。 - 异步队列溢出:如果日志产生速度超过写入速度,队列满了会丢弃日志。可以调大队列大小,或者使用阻塞模式。
- 多线程竞争:多个线程写同一个文件logger,一定要用
_mt版本,否则会数据错乱。 - 日志文件路径:确保日志目录存在,spdlog不会自动创建目录。我一般会在程序启动时检查并创建。
知识体系总览
下面这张图总结了spdlog的核心概念和集成流程,方便你快速回顾:
核心要点总结:
- 集成spdlog最推荐用FetchContent,一行命令搞定依赖
- 链接时用
spdlog::spdlog(编译模式)或spdlog::spdlog_header_only(头文件模式) - 生产环境务必使用异步日志,避免日志拖慢业务
- 多sink组合可以实现不同输出目标的不同级别控制
- 记得设置flush策略,防止崩溃时丢失日志
好了,关于spdlog的集成就聊到这儿。其实日志库本身不复杂,但用好了能省很多排查问题的时间。我个人建议你从一个小项目开始,把spdlog集成进去,跑一跑异步和同步的对比,感受一下性能差异。嗯,动手试试吧。