☰
spdlog入门到实践:C++日志库安装、异步与MFC集成指南
2026/10/25 19:47:20 网站建设 项目流程

spdlog 是 C++ 社区里使用率很高的一款日志库,核心卖点是“header-only 也能用、性能高、上手快”。很多 C++ 开发者第一次接触它,是为了替换自己手写的printf日志或者早期自封装日志类;也有很多 MFC 项目在维护旧代码时,想把日志模块从混乱的输出窗口打印切换成文件日志。这篇文章会围绕 spdlog 的安装方式、基础 API、异步日志、文件滚动、MFC 集成、常见编译错误和性能调优展开,目标是把一个最小可用日志系统完整跑起来,并解释清楚每一步背后的机制。

spdlog 之所以受欢迎,主要有三个原因。第一,它支持纯头文件模式,直接把include/spdlog复制到项目里就能用,省去链接第三方库的麻烦。第二,它基于 C++11 标准实现,现代编译器基本都能编译。第三,它的日志输出格式、文件名滚动、异步线程池、多接收器(sink)设计非常清晰,适合从简单到复杂的各种场景。

本文会先解释 spdlog 的核心概念,再给出 Windows 和 Linux 两种环境下的编译准备方式,然后写一个最基础的示例,再逐步加入文件日志、滚动文件、异步模式,最后单独用一节说明 MFC 项目里如何集成 spdlog,并提供常见编译错误速查表和排查路径。读完这篇文章,你应该能独立完成一个 C++ 工程的日志模块改造。

1. 先理解 spdlog 的日志模型,再写第一行代码

1.1 spdlog 中的 logger、sink、formatter 分别负责什么

任何日志库都可以拆成三个职责:日志接口、日志输出目标、日志格式化。spdlog 分别用 logger、sink、formatter 来表达。

logger 是开发者直接操作的对象,相当于一个“日志器实例”。它负责接收info()、warn()、error()等调用,并判断当前日志级别是否应该输出。如果你的程序里有业务模块、网络模块、数据库模块,可以给每个模块创建独立的 logger,日志写入同一个文件但带上不同名字,方便过滤。

sink 是真正的输出终点。spdlog 提供了多种 sink:

sink 类型输出目标常见场景
stdout_sink标准输出调试窗口、控制台程序
basic_file_sink单个文件简单文件日志
rotating_file_sink滚动文件按文件大小拆分
daily_file_sink按天滚动文件服务端长期运行
msvc_sinkVisual Studio 输出窗口Windows GUI/MFC 调试

一个 logger 可以挂多个 sink,比如同时输出到控制台和文件,这在排查问题时会很有用。默认 logger 是spdlog::logger("default_logger")的一个全局实例,直接调用spdlog::info()就是在使用它。

formatter 负责把日志内容拼成最终字符串。spdlog 默认格式类似于:

[2025-01-01 12:00:00.123] [info] [example.cc:42] hello

其中[example.cc:42]是通过source_loc拿到的源码位置,需要在编译时开启SPDLOG_ACTIVE_LEVEL宏才能保证位置信息被记录。

1.2 编译期级别宏与运行时级别的关系

spdlog 有一个容易混淆的设计:编译期级别和运行时级别是两套机制。

编译期级别由SPDLOG_ACTIVE_LEVEL宏控制,默认是SPDLOG_LEVEL_INFO。低于该级别的日志调用,在编译阶段就会被优化掉。比如把宏设为SPDLOG_LEVEL_WARN,那么spdlog::info()连函数体都不会生成,这样可以减少二进制体积和运行开销。

运行时级别通过logger->set_level()设置。比如开发阶段把某个模块的日志级别设为 debug,生产环境再调回 info。compile-time 过滤发生在代码生成前,runtime 过滤发生在每次调用前。

实际项目里推荐这样处理:

// 在包含 spdlog.h 之前定义 #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG #include <spdlog/spdlog.h> #include <spdlog/sinks/basic_file_sink.h>

如果只包含头文件,不依赖 spdlog 的预编译库,所有功能都会被编译进当前翻译单元,首次编译会稍慢,但增量编译其实还好。

1.3 默认 logger 与自定义 logger 的取舍

直接使用全局默认 logger 的写法是:

spdlog::info("Hello, {}", "spdlog");

这种写法适合小型工具和小型示例。它的问题在于全局唯一,所有模块都共用同一个日志实例,无法按模块分别控制级别或输出文件。稍微复杂一点的项目,应该创建自定义 logger。

创建自定义 logger 的常用方式:

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/app.log", true); auto logger = std::make_shared<spdlog::logger>("multi_sink", std::initializer_list<spdlog::sink_ptr>{console_sink, file_sink}); spdlog::register_logger(logger);

这里_mt后缀代表多线程安全版本。如果项目是单线程程序,可以用_st版本减少锁开销;但绝大多数场景直接使用_mt更稳妥,因为后续接网络请求或定时器时不至于改日志库。

2. 环境准备与依赖说明

2.1 编译器和构建系统要求

spdlog 官方要求 C++11 或更高版本。较新的版本(比如 1.12 以上)在部分功能上要求 C++17,但基础日志功能 C++11 即可。因此落地前一定要先看自己使用的 spdlog 版本和 README 中的标准要求。

这里区分几个环境:

环境编译器建议标准
Linux GCCg++ 7 及以上C++11 起步,建议 C++17
Linux Clangclang 6 及以上C++11 起步,建议 C++17
Windows MSVCVS2017 及以上C++14 起步,建议 C++17
Windows MinGW较新版本C++11 起步,建议 C++17

如果项目里已经使用了 C++17,直接使用-std=c++17编译不会有什么问题。如果还在维护 C++11 老项目,就要选择与 C++11 兼容的 spdlog 版本,或者谨慎升级。

2.2 方式一:纯头文件模式

下载 spdlog 源码后,目录结构大致如下:

spdlog/ include/ spdlog/ spdlog.h ... src/ spdlog.cpp ... CMakeLists.txt

在 CMake 工程中,header-only 模式最简单的引用方式:

add_executable(demo main.cpp) target_include_directories(demo PRIVATE "path/to/spdlog/include") target_compile_features(demo PRIVATE cxx_std_17)

如果使用 g++ 直接编译:

g++ -std=c++17 -I./spdlog/include main.cpp -o demo

header-only 模式不需要链接额外的.a或.lib文件,因为日志库的所有实现都在头文件里。但编译单元会膨胀,首次编译时间偏长。

2.3 方式二:预编译源码模式

如果希望减少编译时间,或者需要把 spdlog 编译成动态库/静态库,可以编译src/spdlog.cpp。

使用 CMake 时,可以让项目启用SPDLOG_COMPILED_LIB:

set(SPDLOG_COMPILED_LIB ON) add_subdirectory(path/to/spdlog) target_link_libraries(demo PRIVATE spdlog::spdlog)

也可以手动在源文件中包含编译实现:

#include "spdlog/spdlog.h" // 在某个 .cpp 里加入 #include "spdlog/sinks/basic_file_sink.cpp"

不过这种手动包含.cpp的方式不够优雅,推荐使用 CMake 的add_subdirectory。

2.4 方式三:包管理器安装

在 Linux 上使用 vcpkg 或 apt 都可以。需要说明,apt 安装的 spdlog 版本可能较旧,如果项目必须使用新特性,建议源码集成。

# vcpkg vcpkg install spdlog # Ubuntu/Debian 示例 sudo apt install libspdlog-dev

使用 vcpkg 时,CMake 需要通过 toolchain 文件找到包:

cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[vcpkg-root]/scripts/buildsystems/vcpkg.cmake

重点提醒:如果项目使用纯头文件模式,不要同时链接spdlog::spdlog编译库,否则可能出现符号重复定义或 ABL 不匹配的链接错误。选择一个模式并保持一致性。

3. 第一个可运行示例:控制台输出与基本格式化

3.1 最小控制台示例

创建main.cpp:

#include <spdlog/spdlog.h> int main() { spdlog::info("Welcome to spdlog"); spdlog::warn("This is a warning, value = {}", 42); spdlog::error("Error code: {}", -1); return 0; }

编译并运行:

g++ -std=c++17 -I./spdlog/include main.cpp -o demo ./demo

预期输出类似于:

[2025-01-01 10:00:00.123] [info] Welcome to spdlog [2025-01-01 10:00:00.123] [warning] This is a warning, value = 42 [2025-01-01 10:00:00.123] [error] Error code: -1

这里有一个重要细节:spdlog::warn()的日志级别名称在控制台显示为[warning],这是默认 pattern 的规则。如果你在文件里看到[warning],不要觉得是拼写错误。

3.2 占位符语法与常见误用

spdlog 使用{}占位符,和 C++20 的std::format风格类似。不要在{}内部写%d、%s,那会原样输出。

int id = 100; std::string name = "order"; spdlog::info("id={}, name={}", id, name); // 错误示例:spdlog::info("id=%d, name=%s", id, name);

占位符数量必须和参数数量一致。参数少一个不会在编译期报错,但会在运行时输出格式异常或抛出异常。生产代码中如果不确定,优先把日志拆分,避免一条日志里拼太多变量。

3.3 自定义 pattern 的推荐格式

日志格式会影响日志排查效率。推荐包含时间、级别、线程 id、源码位置、消息内容:

spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%t] [%l] [%s:%#] %v");

常见 pattern 标记速查:

标记含义
%Y年
%m月
%d日
%H:%M:%S时分秒
%e毫秒
%t线程 id
%l日志级别
%s源文件名
%#源文件行号
%v日志正文

源码位置信息只有在调用SPDLOG_LOGGER_INFO这类宏时才会被正确填充。如果直接调用logger->info(),默认也能拿到位置,但宏方式可以在编译期就过滤掉低级别日志,性能更好。

实际项目中常用宏封装:

#define LOG_INFO(...) SPDLOG_LOGGER_INFO(::spdlog::default_logger(), __VA_ARGS__) #define LOG_ERROR(...) SPDLOG_LOGGER_ERROR(::spdlog::default_logger(), __VA_ARGS__)

如果希望每个模块有自己的 logger,可以把宏改成传入 logger 指针的版本。

4. 文件日志:basic_file_sink 与 rotating_file_sink

4.1 基础文件日志

单文件日志适合开发阶段或短时间运行的脚本。使用basic_file_sink_mt时,第二个参数是是否追加。

#include <spdlog/spdlog.h> #include <spdlog/sinks/basic_file_sink.h> int main() { auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/app.log", true); spdlog::set_default_logger(file_logger); file_logger->info("write to file"); file_logger->flush_on(spdlog::level::info); return 0; }

flush_on表示在指定级别时立即刷新缓冲区。如果程序崩溃,没有刷新到磁盘的日志会丢失。生产环境至少把error级别设置为自动刷新,同时定期调用logger->flush()。

4.2 滚动文件日志

长时间运行的服务如果只写单个文件,磁盘会被撑满,排查时也不方便定位某一天的日志。spdlog 的rotating_file_sink在文件达到指定大小后,滚动生成新文件,并保留最多一定数量的历史文件。

#include <spdlog/spdlog.h> #include <spdlog/sinks/rotating_file_sink.h> int main() { // 文件名为 app.log,单个文件最大 5MB,最多保留 3 个历史文件 auto logger = spdlog::rotating_logger_mt("rotating_logger", "logs/rotate.log", 1024 * 1024 * 5, 3); logger->info("rolling file log"); return 0; }

滚动后文件会变成类似:

rotate.log rotate.1.log rotate.2.log rotate.3.log

max_size参数过小会导致频繁滚动,过大会导致单文件太大。建议根据单条日志平均大小和日志量估算。例如每分钟 1MB 日志,单文件设 100MB 比较合理。

4.3 按天滚动文件日志

服务端程序通常适合按天滚动,保留最近 N 天日志。daily_file_sink需要指定几点进行切换。

#include <spdlog/spdlog.h> #include <spdlog/sinks/daily_file_sink.h> int main() { auto logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.log", 0, 0); logger->info("daily log message"); return 0; }

daily_file_sink的构造函数参数是时、分,0 点 0 分切换文件。它会生成类似daily_2025-01-01.log的文件。按天滚动在排查线上问题时非常有用,比如直接查看某一天的日志文件。

4.4 多 sink 组合:同时写控制台和文件

很多时候调试时需要看到控制台输出,同时保存文件日志。可以用sinks初始化列表组合多个 sink。

#include <spdlog/spdlog.h> #include <spdlog/sinks/stdout_color_sinks.h> #include <spdlog/sinks/rotating_file_sink.h> int main() { auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("logs/multi.log", 1024 * 1024 * 5, 3); std::vector<spdlog::sink_ptr> sinks{console_sink, file_sink}; auto logger = std::make_shared<spdlog::logger>("multi", sinks.begin(), sinks.end()); spdlog::set_default_logger(logger); logger->info("multi sink message"); return 0; }

此时同一条日志会同时写入控制台和文件,文件里不包含 ANSI 颜色码,因为颜色是由stdout_color_sink_mt单独处理的。这个设计在 spdlog 里非常清晰:sink 之间互不干扰。

5. 异步日志与性能调优

5.1 为什么需要异步日志

同步日志在调用logger->info()时,会直接执行格式化、sink 写入、可能还有文件 flush。如果日志量大,日志写入会拖慢业务线程。异步日志会把日志消息先放入队列,再由后台线程统一写入文件,业务线程只负责放入队列,可以显著减少日志对业务逻辑的影响。

spdlog 提供async_logger。使用前需要初始化一个thread_pool:

#include <spdlog/spdlog.h> #include <spdlog/async.h> #include <spdlog/sinks/rotating_file_sink.h> int main() { // 队列大小 8192,后台线程数 1 spdlog::init_thread_pool(8192, 1); auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("logs/async.log", 1024 * 1024 * 5, 3); auto logger = std::make_shared<spdlog::async_logger>( "async_logger", file_sink, spdlog::thread_pool(), spdlog::async_overflow_policy::block); spdlog::set_default_logger(logger); for (int i = 0; i < 1000; ++i) { logger->info("async message {}", i); } spdlog::shutdown(); return 0; }

5.2 队列溢出策略

异步日志队列是有限的。如果日志产生速度大于后台线程消费速度,队列会被塞满。async_overflow_policy有两种常见策略:

策略行为适用场景
block队列满时业务线程阻塞等待,直到有空间日志不能丢,但可能拖慢业务
overrun_oldest丢弃最旧的日志,保证新日志能入队日志可以丢,但要保证最新日志

生产环境建议使用block作为默认策略,同时把队列大小设置得足够大。如果担心日志丢失,可以在shutdown()前调用logger->flush()。

5.3 同步与异步的性能表现

性能测试结果依赖硬件、线程数、日志格式和磁盘类型。spdlog 官方描述其同步日志性能很高,异步日志适合吞吐量大的场景。不要盲信任何一个具体数值,最稳妥的做法是在自己项目里用相同日志格式、相同输出路径做压测,对比业务线程的耗时和日志吞吐量。

异步模式的代价是实时性下降。程序崩溃时,队列中尚未写入的日志会丢失。因此在需要立刻看到日志的场景,比如热修复、金额计算、安全关键逻辑,不要把这些日志设计成异步,或至少同时追加一条同步的错误日志。

5.4 避免在日志参数里做昂贵计算

即使使用异步日志,参数表达式仍然会在调用线程求值。例如:

logger->info("user info: {}", build_large_json(user));

build_large_json(user)在进入日志库前就会执行。如果这个函数很昂贵,日志系统再快也没用。建议先判断级别再组装日志内容:

if (logger->should_log(spdlog::level::debug)) { logger->debug("user info: {}", build_large_json(user)); }

或者使用SPDLOG_LOGGER_DEBUG宏,它可以配合编译期级别在编译阶段移除低级别日志。

6. MFC 项目集成 spdlog:示例代码与 Visual Studio 配置

6.1 MFC 项目为什么需要日志库

MFC 项目通常是 Windows 桌面程序,很多老代码使用OutputDebugString、TRACE、AfxMessageBox输出运行信息。这些方式在发布版本里不适合定位问题:OutputDebugString需要调试器才能看到,AfxMessageBox会阻塞 UI,TRACE在 Release 下默认无效。用 spdlog 把日志写入文件,可以在用户机器上收集运行轨迹,排查问题更高效。

6.2 配置 Visual Studio 工程

在 VS 中集成 spdlog 的步骤:

  1. 将 spdlog 源码放到项目目录,比如external/spdlog。
  2. 项目属性 -> C/C++ -> 常规 -> 附加包含目录,添加external/spdlog/include。
  3. 如果使用预编译头,建议把 spdlog 头文件包含在stdafx.h或pch.h中,避免每个.cpp重复包含。
  4. 语言标准设置为 C++17,路径在 项目属性 -> C/C++ -> 语言 -> C++ 语言标准。

如果使用纯头文件模式,不需要配置链接库。如果已经编译了spdlog.lib或使用 vcpkg,则需要在链接器输入中添加库文件。

6.3 MFC 项目中的示例代码

以下是一个模拟 MFC 对话框工程中初始化和使用日志的例子。

在stdafx.h或pch.h中加入:

#pragma once #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG #include <spdlog/spdlog.h> #include <spdlog/sinks/basic_file_sink.h> #include <spdlog/sinks/msvc_sink.h>

在CWinApp::InitInstance中初始化日志:

BOOL CMyApp::InitInstance() { // 配置输出到 Visual Studio 输出窗口和文件 auto msvc_sink = std::make_shared<spdlog::sinks::msvc_sink_mt>(); auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>( "logs/myapp.log", 1024 * 1024 * 5, 5); std::vector<spdlog::sink_ptr> sinks{msvc_sink, file_sink}; auto logger = std::make_shared<spdlog::logger>("mfc_app", sinks.begin(), sinks.end()); logger->set_level(spdlog::level::debug); logger->flush_on(spdlog::level::warn); spdlog::set_default_logger(logger); spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%t] [%l] [%s:%#] %v"); LOG_INFO("MFC application started"); return CWinApp::InitInstance(); }

在业务代码中使用:

void CMyDialog::OnBnClickedButton() { try { // 业务逻辑 LOG_INFO("button clicked"); } catch (const std::exception& e) { LOG_ERROR("exception: {}", e.what()); } }

msvc_sink输出到 VS 的“输出”窗口,方便开发时调试;rotating_file_sink写入发布环境的日志文件。这样开发环境和用户现场都能保留日志。

6.4 MFC 中要注意编码问题

spdlog 默认按 UTF-8 处理字符串。MFC 项目中如果使用CString,在日志输出前需要显式转换,尤其是 Unicode 字符集(_UNICODE)下,CString是宽字符类型,不能直接作为{}参数传入 spdlog。

推荐封装一层转换函数:

#include <atlstr.h> std::string ToUtf8(const CString& str) { if (str.IsEmpty()) { return std::string(); } int len = WideCharToMultiByte(CP_UTF8, 0, str.GetString(), str.GetLength(), nullptr, 0, nullptr, nullptr); std::string result(len, '\0'); WideCharToMultiByte(CP_UTF8, 0, str.GetString(), str.GetLength(), &result[0], len, nullptr, nullptr); return result; } // 使用示例 CString userName = L"张三"; LOG_INFO("user name: {}", ToUtf8(userName));

如果不做转换,CString在 spdlog 的格式化机制里可能被当成指针或字符数组处理,轻则输出乱码,重则导致崩溃。这是 MFC 集成中最常见的坑之一。

6.5 MFC 程序中关闭时的清理顺序

如果直接在ExitInstance或窗口销毁代码中调用spdlog::shutdown(),要注意 logger 可能还被其他线程使用。建议在进程即将退出、所有业务线程已经释放后再调用:

int CMyApp::ExitInstance() { spdlog::shutdown(); return CWinApp::ExitInstance(); }

shutdown()会停止异步线程池并刷新所有 logger。如果先退出了异步线程,再调用日志接口,可能出现“logger 已关闭”的异常。因此要保证退出路径上不再有任何日志调用。

7. 常见报错、排查路径与解决方案

7.1 编译错误:找不到头文件

现象:

fatal error C1083: Cannot open include file: 'spdlog/spdlog.h': No such file or directory

检查顺序:

  1. 确认 spdlog 的include目录路径是否写对。常见错误是把spdlog/include/spdlog路径加入 include,导致编译器去查找spdlog/spdlog/spdlog.h。
  2. 确认附加包含目录是否配置到spdlog/include这一层。
  3. 如果项目开启了预编译头,确认头文件是否在预编译头中正确包含。

推荐做法:

// 错误示例 #include <spdlog/spdlog.h> // 检查 include 路径 // 推荐:确认目录结构后配置成 // 附加包含目录:D:\project\external\spdlog\include

7.2 链接错误:符号重复定义

如果同时启用了SPDLOG_HEADER_ONLY和编译库,可能出现重复符号。

现象类似:

LNK2005 spdlog::logger::log already defined in spdlog.lib

原因:spdlog 的函数既在头文件中有 inline 定义,又在编译后的库中有非 inline 定义。

解决方案:不要混合使用。纯头文件模式不要链接编译库,编译库模式不要定义SPDLOG_HEADER_ONLY。

7.3 日志文件没有生成或为空

可能原因和排查顺序:

现象可能原因检查方式处理方案
没有生成文件文件路径目录不存在查看当前工作目录先创建目录,或使用绝对路径
文件生成为空日志级别过滤检查set_level和flush_on调低级别或手动 flush
文件刷新不及时异步队列未刷盘程序退出前未 shutdown调用spdlog::shutdown()
文件权限不足程序没有目录写权限检查日志目录 ACL修改目录权限或更换路径

7.4 运行时异常:spdlog_ex

spdlog 在无法创建文件、无法打开 sink 时会抛出spdlog::spdlog_ex异常。

现象:

terminate called after throwing an instance of 'spdlog::spdlog_ex' what(): Failed to open file ...

解决方案:

  • 启动前创建日志目录。
  • 使用 try-catch 捕获初始化异常,避免程序直接崩溃。
try { auto logger = spdlog::basic_logger_mt("app", "logs/app.log"); spdlog::set_default_logger(logger); } catch (const spdlog::spdlog_ex& e) { // 处理初始化失败,例如回退到控制台日志 spdlog::set_default_logger(spdlog::stdout_logger_mt("console")); spdlog::error("Failed to create file logger: {}", e.what()); }

7.5 日志时间显示不正确

spdlog 默认使用本地时间,UTC 场景需要设置set_pattern中的时间戳来源。如果不特殊设置,%Y-%m-%d %H:%M:%S输出的是本机时间。在容器或服务器环境中,时区不对会导致日志时间和事件时间不一致。

排查方式:先对比本机date命令输出和日志时间,确认是否有几小时偏差。如果是 UTC 日志需求,使用 spdlog 的custom_flag_formatter或自定义 pattern,尽量在程序初始化时统一设置时区处理。

7.6 中文乱码

中文乱码通常源于编码不一致。比如源文件是 GBK,而日志文件按 UTF-8 写入。建议统一采用 UTF-8 编码:

  • 源文件保存为 UTF-8。
  • 使用/utf-8编译选项,VS 中可在 项目属性 -> C/C++ -> 命令行 添加/utf-8。
  • MFC 中的CString显式转 UTF-8。

如果是老项目历史遗留的 GBK 日志,则要保持输出编码与查看工具一致,不要混用。

8. 生产环境最佳实践与扩展方向

8.1 日志目录和文件命名规范

生产环境日志目录应独立于程序安装目录,避免升级卸载时丢失日志。推荐结构:

/var/log/myapp/ myapp_2025-01-01.log myapp_2025-01-02.log

Windows 服务程序可以放在C:\ProgramData\MyApp\logs而不是安装目录。程序启动时通过GetModuleFileName或配置文件路径拼出日志目录,不要在代码里硬编码“当前目录”。

8.2 日志级别切换的工程手段

不要在发布版本里手动改代码来切换日志级别。建议从配置文件或环境变量读取。

std::string level_str = GetConfigValue("log_level", "info"); spdlog::level::level_enum level = spdlog::level::from_str(level_str); logger->set_level(level);

这样可以做到不动代码、不重新编译,在用户现场把日志级别临时调低,定位问题后再调回。

8.3 日志脱敏

用户信息、token、密钥、手机号、身份证号不能直接写入日志。可以在封装层做脱敏处理:

std::string MaskPhone(const std::string& phone) { if (phone.size() < 7) { return "****"; } return phone.substr(0, 3) + "****" + phone.substr(7); }

然后在日志输出前调用。

8.4 日志监控与告警

文件日志只是基础,生产环境还需要采集和告警。spdlog 输出的结构化格式如果设计得好,可以通过 filebeat、fluentd 等工具采集到日志平台。关键点:

  • 固定 pattern,方便解析。
  • 使用 JSON sink 输出结构化日志。
  • 记录 trace id、request id 用于链路追踪。

spdlog 本身不包含日志采集和告警能力,但它的 sink 开放接口允许接入自定义 sink。比如可以写一个 HTTP sink 把错误日志推送到内网告警服务。

8.5 自定义 sink 的扩展思路

如果项目需要把日志写入数据库、发送到消息队列、转发到远程日志服务,可以实现spdlog::sinks::base_sink。

#include <spdlog/sinks/base_sink.h> class my_http_sink final : public spdlog::sinks::base_sink<std::mutex> { protected: void sink_it_(const spdlog::details::log_msg& msg) override { // 将 msg 格式化并发送到远程 } void flush_() override { // 刷新缓冲区 } };

自定义 sink 时要注意线程安全和性能,不要在sink_it_里做耗时太长的远程调用,否则会阻塞 logger 或异步队列。

8.6 建议的学习路径和练习清单

想真正掌握 spdlog,建议按顺序做下面几个练习:

  1. 用 header-only 模式写一个控制台日志程序。
  2. 加入滚动文件日志,模拟生成 10000 条日志。
  3. 设置自定义 pattern,包含线程 id 和源码位置。
  4. 使用异步模式,观察日志吞吐量和程序退出时的刷新行为。
  5. 在 MFC 或 Qt 程序中集成 spdlog,同时输出到调试窗口和文件。
  6. 编写一个自定义 sink,把 error 级别日志单独写入另一个文件。
  7. 在项目里新增日志脱敏工具类,封装spdlog::info等宏。

完成这些练习后,你会对 sink、logger、异步队列、编译期级别、运行时级别这些概念形成自己的理解,后续遇到日志丢失、文件滚动异常、编码乱码等问题时,也能更快定位到具体环节。

9. 总结与关键选择

spdlog 不是一个很复杂的技术,但它的设计选择值得认真理解。对大多数项目,推荐使用纯头文件模式起步,把日志封装在统一模块中,用rotating_file_sink或daily_file_sink作为文件输出,同时挂一个调试窗口 sink 方便开发。等到日志量增大,再切换到异步模式,并根据业务特点选择队列溢出策略。

最重要的一点是:日志系统不是写完初始化代码就结束了,它需要根据实际运行情况持续调整。日志文件增长是否过快、异步队列是否经常满、日志级别是否需要远程切换、敏感信息是否被写入文件,这些都是上线后要持续观察的问题。先在开发环境把日志链路完整跑通,再逐步加上文件滚动、异步、脱敏和监控,才是比较稳妥的推进方式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询