第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_onwarnerr级别。
  • 异步队列溢出:如果日志产生速度超过写入速度,队列满了会丢弃日志。可以调大队列大小,或者使用阻塞模式。
  • 多线程竞争:多个线程写同一个文件logger,一定要用_mt版本,否则会数据错乱。
  • 日志文件路径:确保日志目录存在,spdlog不会自动创建目录。我一般会在程序启动时检查并创建。

知识体系总览

下面这张图总结了spdlog的核心概念和集成流程,方便你快速回顾:

spdlog 集成知识体系 CMake集成 FetchContent find_package add_subdirectory Logger(日志器) Sink(输出目标) Formatter(格式化) Level(级别) 同步/异步 单线程/多线程 控制台/文件/轮转 syslog/自定义 时间/线程/文件 颜色/自定义格式 trace/debug/info warn/error/critical target_link_libraries(my_app PRIVATE spdlog::spdlog)

核心要点总结

  1. 集成spdlog最推荐用FetchContent,一行命令搞定依赖
  2. 链接时用spdlog::spdlog(编译模式)或spdlog::spdlog_header_only(头文件模式)
  3. 生产环境务必使用异步日志,避免日志拖慢业务
  4. 多sink组合可以实现不同输出目标的不同级别控制
  5. 记得设置flush策略,防止崩溃时丢失日志

好了,关于spdlog的集成就聊到这儿。其实日志库本身不复杂,但用好了能省很多排查问题的时间。我个人建议你从一个小项目开始,把spdlog集成进去,跑一跑异步和同步的对比,感受一下性能差异。嗯,动手试试吧。


公众号:蓝海资料掘金营,微信deep3321