Linux C++项目集成spdlog日志库:从基础配置到异步日志与性能优化

发布时间:2026/7/15 19:31:18
Linux C++项目集成spdlog日志库:从基础配置到异步日志与性能优化 1. 项目概述为什么我们需要一个现代的C日志库如果你在Linux上用C写过项目尤其是那些需要长时间运行的服务端程序那么对日志系统的重要性一定深有体会。调试一个线上问题最怕的就是日志要么没有要么混乱不堪要么打日志直接把程序性能拖垮。早期我们可能用printf或者std::cout凑合再后来用syslog但这些方案在复杂的多线程、高性能场景下总是显得捉襟见肘。日志库的核心诉求很简单快、稳、好用。快意味着不能因为打日志而成为性能瓶颈稳要线程安全不能打日志打到程序崩溃好用配置要灵活输出要美观接入要简单。这就是spdlog出现的背景。它是一个纯头文件的C日志库以其极致的速度和丰富的功能迅速成为了C社区的事实标准之一。我第一次接触它是在一个高并发的网络服务项目中当时自研的日志模块在压力测试下出现了丢日志和性能抖动换成spdlog后问题迎刃而解。它的“快”并非空穴来风其异步日志模式能将日志写入的耗时从主业务线程中完全剥离这对于延迟敏感的应用来说是至关重要的。同时它提供了文件、控制台、系统日志syslog等多种输出sink支持每日滚动、按大小滚动等实用的日志管理功能并且格式定制非常自由。所以这篇内容就是带你从零开始在Linux环境下为你的C项目接入spdlog。无论你是在开发一个后台守护进程、一个高性能计算程序还是一个普通的工具软件一个靠谱的日志系统都是基础设施的第一步。我们会从最基础的安装、配置讲起一直深入到异步日志、自定义格式、多sink组合等高级用法并分享一些我在实际项目中踩过的坑和调试技巧。2. 环境准备与spdlog安装在开始写代码之前我们得先把spdlog请到我们的项目里。在Linux环境下安装方式非常灵活你可以根据项目管理的习惯来选择。2.1 安装方式选择包管理器 vs 源码集成方式一使用系统包管理器推荐用于快速原型和学习这是最省事的方法。如果你的系统是Ubuntu/Debian系或者Fedora/RHEL系并且项目不介意依赖系统级的库可以直接用apt或yum安装。# Ubuntu/Debian sudo apt update sudo apt install libspdlog-dev # Fedora/RHEL/CentOS (需要启用EPEL仓库) sudo yum install spdlog-devel安装后头文件通常在/usr/include/spdlog编译时只需要加-lspdlog链接即可。这种方式的好处是干净版本统一。但缺点是系统仓库的版本可能不是最新的如果你需要某个新版本的特有功能就得用其他方法。方式二作为子模块Git Submodule集成到项目中推荐用于生产项目对于严肃的项目我强烈推荐这种方式。它能将依赖的版本锁定在你的项目仓库内确保所有开发者和构建环境的一致性真正做到“开箱即建”。# 在你的项目根目录下 git submodule add https://github.com/gabime/spdlog.git extern/spdlog git submodule update --init --recursive这样spdlog的源码就位于你项目的extern/spdlog目录下了。后续在CMakeLists.txt中你可以用add_subdirectory(extern/spdlog)将它引入并target_link_libraries(your_target PRIVATE spdlog::spdlog)进行链接。因为spdlog是header-only的这里的链接主要作用是传递正确的编译定义和包含路径。方式三直接复制头文件最简易对于小型或一次性项目你也可以直接下载spdlog的release包将其include目录下的所有内容复制到你项目的某个目录比如third_party/spdlog/include然后在编译时用-I指定这个路径。但这种方式缺少版本管理不推荐团队协作使用。注意无论用哪种方式请确保你的编译器支持C11或更高标准。在CMake中通常通过set(CMAKE_CXX_STANDARD 11)或14、17来设置。2.2 一个极简的CMake项目示例为了后续演示方便我们假设创建一个全新的项目。项目结构如下my_log_project/ ├── CMakeLists.txt ├── extern/ # 存放第三方库如果用submodule方式 │ └── spdlog/ # spdlog源码 └── src/ └── main.cpp # 我们的主程序对应的CMakeLists.txt可以这样写以submodule方式为例cmake_minimum_required(VERSION 3.10) project(MyLogProject) set(CMAKE_CXX_STANDARD 17) # 添加spdlog子目录 add_subdirectory(extern/spdlog) # 添加可执行文件 add_executable(log_demo src/main.cpp) # 链接spdlog目标 target_link_libraries(log_demo PRIVATE spdlog::spdlog)这样一个最基本的支持spdlog的编译环境就搭建好了。接下来我们进入src/main.cpp开始真正的日志之旅。3. 基础使用从“Hello Log”开始让我们从一个最简单的例子开始感受一下spdlog的基本用法。打开src/main.cpp写入以下代码#include iostream #include spdlog/spdlog.h int main() { // 初始化一个控制台日志器日志级别为info auto console_logger spdlog::stdout_color_mt(console); console_logger-set_level(spdlog::level::info); // 打印不同级别的日志 console_logger-trace(这是一条trace级别日志通常用于最详细的调试信息。); console_logger-debug(这是一条debug级别日志。); console_logger-info(欢迎使用spdlog); console_logger-warn(这是一条警告信息表示可能有问题。); console_logger-error(这是一条错误信息); console_logger-critical(这是一条严重错误信息程序可能即将终止。); // 也可以使用全局的默认日志器更简洁 spdlog::info(使用全局默认日志器输出info日志。); spdlog::error(这是一条全局错误日志。); // 格式化日志 int user_id 1001; double response_time 12.345; console_logger-info(用户 [{}] 的请求处理耗时 {:.2f} 毫秒。, user_id, response_time); return 0; }编译并运行这个程序你会在终端看到彩色的输出如果你的终端支持颜色。类似这样[2024-05-27 10:30:15.123] [console] [info] 欢迎使用spdlog [2024-05-27 10:30:15.123] [console] [warning] 这是一条警告信息表示可能有问题。 [2024-05:30:15.123] [console] [error] 这是一条错误信息 ...你会发现trace和debug级别的日志没有输出。这是因为我们通过set_level(spdlog::level::info)将日志级别设置为了info只有级别等于或高于info即info、warn、error、critical的日志才会被实际记录。这是日志系统的一个核心机制可以在开发时输出详细日志线上则只记录错误和关键信息通过一个开关日志级别来控制。几个关键点解析日志器Loggerspdlog::stdout_color_mt(“console”)创建了一个名为“console”的日志器。mt后缀代表“multi-threaded”多线程安全这是最常用的。每个日志器可以独立设置级别、输出格式和输出目标sink。日志级别从低到高依次是trace,debug,info,warn,error,critical,off。设置级别可以过滤掉低级别的日志。格式化spdlog使用了类似Pythonstr.format或C20std::format的语法{}作为占位符支持各种类型的格式化如{:.2f}表示保留两位小数。这种方式比C的printf更安全类型安全也比流操作符更高效。全局默认日志器spdlog::info(“…” )这种写法使用的是spdlog库内部一个全局的默认日志器。对于简单的单文件程序很方便但对于复杂项目建议创建具名日志器以便管理。4. 核心功能深入文件日志、异步日志与格式定制只会往控制台打印彩色日志是远远不够的。生产环境更需要将日志持久化到文件并且不能影响主程序性能。同时日志的格式也需要根据团队规范或日志分析系统的要求进行定制。4.1 输出到文件与日志滚动将日志写入文件是最基本的需求。spdlog提供了非常方便的文件sink并且内置了日志滚动功能可以防止单个日志文件无限膨胀。#include “spdlog/spdlog.h” #include “spdlog/sinks/rotating_file_sink.h” // 按大小滚动的sink #include “spdlog/sinks/daily_file_sink.h” // 按天滚动的sink int main() { // 1. 创建一个基本的文件日志器无滚动慎用 auto basic_file_logger spdlog::basic_logger_mt(“basic_file”, “logs/basic.log”); basic_file_logger-info(“这条日志会写入 logs/basic.log 文件。”); // 2. 创建一个按文件大小滚动的日志器最常用 // 参数日志器名 文件名 最大文件大小 保留的备份文件数量 auto rotating_logger spdlog::rotating_logger_mt(“rotating”, “logs/myapp.log”, 1048576 * 5, 3); rotating_logger-set_level(spdlog::level::debug); rotating_logger-info(“这条日志写入 myapp.log。当文件达到5MB时会滚动。”); // 模拟写入大量日志触发滚动 for(int i 0; i 10000; i) { rotating_logger-debug(“Log entry {}” i); } // 最终会得到: myapp.log (当前), myapp.log.1, myapp.log.2, myapp.log.3 (最旧) // 3. 创建一个每日滚动的日志器 // 参数日志器名 文件名前缀 每日触发时间(小时,分钟) 是否删除旧日志 保留天数(0为不删除) auto daily_logger spdlog::daily_logger_mt(“daily”, “logs/daily_log”, 2, 30); // 每天凌晨2:30创建新文件 daily_logger-info(“这条日志会写入类似 logs/daily_log-2024-05-27.log 的文件。”); // 重要程序结束前确保所有日志被刷新到磁盘 spdlog::shutdown(); return 0; }实操心得滚动策略选择rotating_logger按大小滚动适用于日志量不稳定但需要限制磁盘占用的场景。daily_logger按天滚动非常适用于按天进行日志分析和归档的场景比如很多运维监控系统默认按天切分日志。文件路径确保程序对日志目录如logs/有写入权限否则创建日志器时会抛出异常。最好在程序启动时检查或创建目录。spdlog::shutdown()在main函数返回前调用此函数会刷新所有日志并释放资源。虽然很多情况下不调用也不会丢日志因为析构函数会处理但在一些复杂退出场景如调用exit下显式调用更安全。4.2 异步日志性能的关键同步日志意味着每次调用logger-info(…)时当前线程都要等待I/O操作写文件、刷控制台完成。在高频日志场景下这会造成严重的性能卡顿。异步日志将日志消息放入一个队列由一个独立的后台线程负责取出并写入业务线程在放入队列后就可以立即返回从而极大提升性能。#include “spdlog/spdlog.h” #include “spdlog/async.h” // 异步日志头文件 #include “spdlog/sinks/rotating_file_sink.h” int main() { // 设置异步日志的全局参数通常在程序初始化时做一次 // 队列大小条数8192 后台线程数1 当队列满时是阻塞(block)还是丢弃新日志(discard) spdlog::init_thread_pool(8192, 1); // 队列大小要合理太小易丢日志太大会占内存 // 创建异步的文件sink auto async_file_sink std::make_sharedspdlog::sinks::rotating_file_sink_mt(“logs/async_app.log”, 1048576*10, 5); // 使用异步工厂函数创建日志器 auto async_logger std::make_sharedspdlog::async_logger(“async_logger”, async_file_sink, spdlog::thread_pool(), spdlog::async_overflow_policy::block); async_logger-set_level(spdlog::level::info); spdlog::register_logger(async_logger); // 注册到全局方便通过 spdlog::get(“async_logger”) 获取 // 现在可以像普通日志器一样使用但写入是非阻塞的 for (int i 0; i 100000; i) { async_logger-info(“异步日志消息 {}” i); // 主线程几乎不会在这里阻塞 } // 也可以直接使用辅助函数创建异步日志器更简洁 auto async_logger2 spdlog::create_asyncspdlog::sinks::rotating_file_sink_mt( “async_logger2”, “logs/async2.log”, 1048576*5, 3 ); spdlog::shutdown(); return 0; }注意事项队列溢出策略spdlog::async_overflow_policy::block表示队列满时生产者业务线程会阻塞等待直到队列有空间。这能保证不丢日志但可能影响业务线程。另一种策略是discard会丢弃最新的日志保证业务流畅但可能丢日志。根据你的业务对日志完整性的要求来选择。性能权衡异步日志虽然快但存在极小的延迟日志消息从入队到被写入磁盘有几毫秒到几十毫秒的延迟。对于要求日志必须立即落盘才能进行下一步操作的场景极少见需要谨慎评估或使用同步模式并配合文件同步操作。内存占用队列大小如8192乘以平均日志消息大小决定了异步日志的内存开销。需要根据日志流量合理设置。4.3 自定义日志格式默认的日志格式[日期时间] [日志器名] [级别] 消息体可能不符合你的需求。spdlog提供了强大的模式字符串pattern string来定制格式。#include “spdlog/spdlog.h” #include “spdlog/sinks/stdout_color_sinks.h” int main() { auto logger spdlog::stdout_color_mt(“custom_format”); // 1. 设置自定义格式 // %Y: 年 %m: 月 %d: 日 %H: 时 %M: 分 %S: 秒 %e: 毫秒 // %n: 日志器名 %l: 级别 %v: 实际消息 %t: 线程ID %P: 进程ID %s: 源文件名 %#: 行号 logger-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%t] [%s:%#] %v”); logger-info(“这是一条带有线程ID、文件名和行号的信息。”); // 输出类似[2024-05-27 14:25:30.456] [info] [140245322331968] [main.cpp:25] 这是一条... // 2. 更简洁的格式适合生产环境监控 logger-set_pattern(“%Y%m%d %H:%M:%S.%f | %-5l | %-15n | %v”); logger-warn(“服务端口检测失败。”); // 输出类似20240527 14:25:30.456123 | WARN | custom_format | 服务端口检测失败。 // 3. 为不同sink设置不同格式例如控制台带颜色文件不带颜色 auto console_sink std::make_sharedspdlog::sinks::stdout_color_sink_mt(); console_sink-set_pattern(“[%Y-%m-%d %H:%M:%S] [%^%l%$] %v”); // 控制台用彩色级别 auto file_sink std::make_sharedspdlog::sinks::rotating_file_sink_mt(“logs/multi_sink.log”, 1048576, 5); file_sink-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] %v”); // 文件用更详细的格式 auto multi_sink_logger std::make_sharedspdlog::logger(“multi_sink”, {console_sink file_sink}); multi_sink_logger-set_level(spdlog::level::info); spdlog::register_logger(multi_sink_logger); multi_sink_logger-info(“这条消息会同时输出到控制台彩色和文件详细。”); return 0; }格式符号详解%^和%$这是一对颜色范围标记。它们之间的内容通常是%l日志级别会在支持颜色的终端上显示为对应级别的颜色如error是红色。%-5l-5表示左对齐且最小宽度为5个字符这样info和warn就能对齐使日志列更整齐。%f微秒6位。%e是毫秒3位。根据你对时间精度的要求选择。%s和%#源文件名和行号。注意要启用这两个选项必须在编译时定义宏SPDLOG_ACTIVE_LEVEL并启用SPDLOG_TRACE宏或者使用SPDLOG_LOGGER_TRACE等宏。直接使用logger-info()时%s和%#会指向spdlog内部代码而非你的调用处。正确用法见下文。5. 高级技巧与最佳实践掌握了基础功能后一些高级用法和最佳实践能让你的日志系统更健壮、更好用。5.1 条件日志与编译期优化频繁的日志调用即使因为级别不够不被输出其参数构造如字符串拼接、复杂对象序列化也可能带来开销。spdlog提供了宏可以将这些开销在编译期就优化掉。#include “spdlog/spdlog.h” // 要使用带源位置的宏需要定义 SPDLOG_ACTIVE_LEVEL并包含 spdlog.h 之后包含 spdlog/fmt/fmt.h 吗不直接包含 spdlog.h 即可它内部已处理。 // 但为了使用 SPDLOG_LOGGER_CALL需要启用宏。更简单的方法是使用下面提供的宏。 // 定义活跃的日志级别。如果设置为 SPDLOG_LEVEL_INFO则 SPDLOG_LOGGER_DEBUG 等更低级别的调用在编译时就是空操作。 #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO #include “spdlog/spdlog.h” int expensive_computation() { // 模拟一个耗时的计算 std::this_thread::sleep_for(std::chrono::milliseconds(10)); return 42; } int main() { auto logger spdlog::stdout_color_mt(“macro_logger”); logger-set_level(spdlog::level::info); // 运行时级别也是info // 不推荐的写法即使日志级别高于debugexpensive_computation()也会被执行 // logger-debug(“计算结果{}” expensive_computation()); // 推荐的写法使用宏 // 如果 SPDLOG_ACTIVE_LEVEL 高于 DEBUG下面这行代码在编译后根本不存在。 SPDLOG_LOGGER_DEBUG(logger, “计算结果{}” expensive_computation()); // 对于全局默认日志器有更简洁的宏 SPDLOG_INFO(“这是一条通过宏输出的info日志。”); // 带格式的宏 int count 5; SPDLOG_LOGGER_WARN(logger, “检测到异常重试当前次数{}” count); // 这些宏会自动捕获调用处的文件名和行号并在格式中包含 %s 和 %# 时正确显示。 logger-set_pattern(“[%s:%#] [%l] %v”); SPDLOG_LOGGER_INFO(logger, “这条日志会显示正确的文件名和行号。”); return 0; }核心要点SPDLOG_ACTIVE_LEVEL这是一个编译期开关。在发布版本中你可以通过编译命令如-DSPDLOG_ACTIVE_LEVELSPDLOG_LEVEL_WARN将其设置为较高的级别如WARN从而将所有更低级别的日志调用从二进制中彻底移除实现零开销。宏 vs 函数调用在性能敏感的代码路径中务必使用SPDLOG_LOGGER_XXX或SPDLOG_XXX宏而不是直接的logger-info()函数调用。这是生产环境日志性能优化的关键一步。5.2 创建多个日志器与分级分类一个复杂的应用程序通常需要多个日志器。例如你可以为不同的模块网络、数据库、业务逻辑创建不同的日志器或者将错误日志和访问日志分开记录。#include “spdlog/spdlog.h” #include “spdlog/sinks/daily_file_sink.h” #include memory class MyApp { private: // 为不同模块声明不同的日志器指针 std::shared_ptrspdlog::logger net_logger_; std::shared_ptrspdlog::logger db_logger_; std::shared_ptrspdlog::logger biz_logger_; std::shared_ptrspdlog::logger access_logger_; public: MyApp() { // 初始化网络模块日志器记录到 net.log级别为debug net_logger_ spdlog::daily_logger_mt(“net”, “logs/net.log”, 0, 0); // 午夜滚动不删除旧日志 net_logger_-set_level(spdlog::level::debug); net_logger_-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%l] %v”); // 初始化数据库模块日志器记录到 db.log级别为info db_logger_ spdlog::daily_logger_mt(“db”, “logs/db.log”, 0, 30); // 保留30天 db_logger_-set_level(spdlog::level::info); db_logger_-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%l] %v”); // 初始化业务逻辑日志器同时输出到控制台和文件 auto console_sink std::make_sharedspdlog::sinks::stdout_color_sink_mt(); auto file_sink std::make_sharedspdlog::sinks::daily_file_sink_mt(“logs/biz.log”, 0, 0); biz_logger_ std::make_sharedspdlog::logger(“biz”, spdlog::sinks_init_list{console_sink, file_sink}); biz_logger_-set_level(spdlog::level::info); // 访问日志使用简单的basic_logger格式为纯CSV便于后续分析 access_logger_ spdlog::basic_logger_mt(“access”, “logs/access.csv”, true); // true表示截断模式打开 access_logger_-set_pattern(“%Y-%m-%d %H:%M:%S,%v”); // CSV格式时间,消息 access_logger_-set_level(spdlog::level::info); } void process_request(const std::string client_ip) { // 记录访问日志 access_logger_-info(“{},GET,/api/user,200,12ms”, client_ip); // 业务逻辑日志 biz_logger_-info(“开始处理来自 {} 的请求。” client_ip); // 模拟数据库操作 db_logger_-debug(“执行查询: SELECT * FROM users WHERE id123”); // ... 数据库操作 db_logger_-info(“查询成功影响行数: 1”); // 模拟网络操作 net_logger_-trace(“建立连接到后端服务...”); // ... 网络操作 net_logger_-warn(“后端服务响应超时准备重试。”); biz_logger_-info(“请求处理完成。”); } }; int main() { MyApp app; app.process_request(“192.168.1.100”); spdlog::drop_all(); // 关闭并删除所有注册的日志器确保日志被刷新 return 0; }这种分门别类的日志管理使得后续的日志监控、分析和问题排查变得非常有针对性。你可以只tail -f logs/net.log来监控网络问题或者用脚本分析access.csv来统计API访问量。5.3 异常处理与日志刷新策略日志系统本身应该是健壮的不能因为日志写入失败而导致程序崩溃。spdlog默认在写入失败时会向stderr输出错误但你可以自定义异常处理器。#include “spdlog/spdlog.h” #include “spdlog/sinks/rotating_file_sink.h” #include iostream void my_custom_exception_handler(const std::string msg) { // 当日志写入发生异常时这个函数会被调用 // 不要在这里面调用任何可能再次抛出异常的日志函数 std::cerr “*** spdlog 日志系统异常: ” msg std::endl; // 你可以在这里将错误发送到监控系统或者采取其他降级措施 } int main() { // 设置自定义异常处理器 spdlog::set_error_handler(my_custom_exception_handler); try { // 故意创建一个无法写入的路径触发异常 auto bad_logger spdlog::rotating_logger_mt(“bad”, “/root/no_permission.log”, 1024, 3); bad_logger-info(“这行日志应该写不进去。”); } catch (const spdlog::spdlog_ex ex) { std::cout “创建日志器失败: ” ex.what() std::endl; } // 日志刷新策略定期自动刷新 vs 手动刷新 auto logger spdlog::rotating_logger_mt(“flush_demo”, “logs/flush.log”, 1024*1024, 1); // 默认情况下spdlog会根据日志级别和sink类型决定刷新频率。 // 对于文件sinkerror及以上级别会立即刷新其他级别可能会缓冲。 // 1. 手动立即刷新确保之前的所有日志都写入磁盘 logger-flush(); // 2. 设置自动刷新级别让info及以上级别也立即刷新更安全但性能略有下降 logger-flush_on(spdlog::level::info); logger-info(“这条info日志会触发自动刷新。”); logger-debug(“这条debug日志可能还在缓冲区。”); // 对于异步日志器flush()会等待队列中所有消息被处理完毕确保所有日志落盘。 spdlog::shutdown(); // shutdown内部会调用所有日志器的flush return 0; }关于刷新Flush的实践经验性能与安全的权衡频繁刷新flush会降低性能因为涉及磁盘I/O。不刷新则可能在程序崩溃时丢失缓冲区中的日志。推荐策略对于大多数应用使用异步日志器并设置flush_on(spdlog::level::warn)或flush_on(spdlog::level::error)是一个不错的折中方案。这样只有警告和错误日志会立即落盘保证关键信息不丢失而大量的info/debug日志则利用缓冲区批量写入提升性能。关键操作前手动刷新在进行一些危险操作如即将执行一个可能崩溃的模块、或程序正常退出前手动调用一次spdlog::shutdown()或全局的spdlog::flush()是良好的习惯。6. 常见问题排查与性能调优即使正确使用了spdlog在实际部署中也可能遇到各种问题。这里记录一些我踩过的坑和解决方案。6.1 问题排查速查表问题现象可能原因排查步骤与解决方案日志文件没有生成1. 目录权限不足。2. 程序工作目录CWD非预期。3. 日志器创建失败但未捕获异常。1. 检查logs/目录是否存在程序是否有写权限ls -ld logs/。2. 在程序启动时打印当前工作目录getcwd。3. 用try-catch包裹日志器创建代码打印异常信息。日志内容丢失尤其是程序崩溃时1. 未调用spdlog::shutdown()或logger-flush()。2. 异步日志队列溢出且策略为discard。3. 缓冲区未刷新。1. 确保主函数退出前调用spdlog::shutdown()。2. 检查异步队列大小如果日志量巨大考虑增大队列或使用block策略。3. 考虑设置flush_on为更低的级别如info。日志输出有乱码或格式错乱1. 终端不支持ANSI颜色码。2. 日志格式字符串有误。3. 多线程输出交织同步日志器在极端情况下也可能发生。1. 对于文件日志不要使用颜色模式%^和%$。2. 仔细检查set_pattern中的格式符确保%匹配正确。3. 确保使用的是_mt多线程安全版本的sink和logger。程序退出时卡住或崩溃1. 静态顺序销毁问题全局或静态日志器。2. 在日志器的异常处理器或自定义sink中抛出了异常。1. 避免使用全局静态日志器。使用shared_ptr并在主函数中显式管理生命周期。2. 使用spdlog::create_async创建异步日志器其生命周期由shared_ptr管理避免手动new。3. 确保自定义错误处理器不会抛出异常。异步日志性能提升不明显1. 日志消息构造本身开销大如频繁进行复杂字符串格式化。2. 队列大小设置不合理导致生产者经常阻塞。1. 使用宏SPDLOG_LOGGER_XXX来避免低级别日志的参数构造开销。2. 检查日志格式是否过于复杂简化模式字符串。3. 适当增加异步线程池的线程数默认为1和队列大小。文件名和行号显示不正确直接使用logger-info()并在格式中包含%s和%#。必须使用SPDLOG_LOGGER_INFO(logger, …)或SPDLOG_INFO(…)等宏来记录日志才能正确捕获调用点的源位置。6.2 性能调优建议Benchmark你的场景spdlog很快但“快”是相对的。如果你每秒要写数十万条日志即使是异步模式也可能成为瓶颈。使用spdlog::benchmark示例代码spdlog源码仓库里有在你的机器上实测一下极限性能。简化日志格式格式越复杂格式化耗时越长。生产环境可以考虑使用更紧凑的格式例如去掉毫秒微秒、去掉线程ID等。避免在日志中序列化大对象如logger-info(“Big object: {}”, some_huge_object);如果some_huge_object的to_string()操作非常耗时即使日志不被输出这个开销也可能发生除非使用宏。尽量只记录关键摘要信息。合理设置日志级别这是最有效的优化。线上环境将级别设为WARN或ERROR可以过滤掉绝大部分日志开销。考虑使用更快的sink对于极限性能场景可以自定义sink比如将日志写入内存环形缓冲区再由另一个线程批量写入文件或网络。spdlog的sink接口设计得很清晰可以自己实现。6.3 一个生产环境可用的配置示例最后分享一个我在实际项目中常用的、相对稳健的日志初始化配置函数它综合了异步、文件滚动、多级别、格式定制等特性#include “spdlog/spdlog.h” #include “spdlog/async.h” #include “spdlog/sinks/rotating_file_sink.h” #include “spdlog/sinks/stdout_color_sinks.h” #include memory #include vector bool setup_logging(const std::string app_name, const std::string log_dir “./logs”) { try { // 0. 创建日志目录 std::filesystem::create_directories(log_dir); // 1. 初始化异步线程池全局一次 // 队列大小每条日志约几百字节根据内存和日志量调整。这里64K条。 // 线程数1个专用写线程通常足够。如果sink操作很慢如远程网络sink可增加。 spdlog::init_thread_pool(65536, 1); // 2. 创建多个sink std::vectorspdlog::sink_ptr sinks; // 控制台sink仅输出WARN及以上级别带颜色 auto console_sink std::make_sharedspdlog::sinks::stdout_color_sink_mt(); console_sink-set_level(spdlog::level::warn); console_sink-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%n] %v”); sinks.push_back(console_sink); // 全量文件sink输出DEBUG及以上级别按100MB大小滚动保留10个文件 std::string all_log_path log_dir “/” app_name “_all.log”; auto file_all_sink std::make_sharedspdlog::sinks::rotating_file_sink_mt(all_log_path, 104857600, 10); file_all_sink-set_level(spdlog::level::debug); file_all_sink-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] [%s:%#] %v”); sinks.push_back(file_all_sink); // 错误文件sink仅输出ERROR及以上级别单独文件便于监控报警 std::string error_log_path log_dir “/” app_name “_error.log”; auto file_error_sink std::make_sharedspdlog::sinks::rotating_file_sink_mt(error_log_path, 10485760, 5); file_error_sink-set_level(spdlog::level::err); file_error_sink-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] [%s:%#] %v”); sinks.push_back(file_error_sink); // 3. 创建异步日志器 auto combined_logger std::make_sharedspdlog::async_logger( app_name, sinks.begin(), sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block // 队列满时阻塞保证不丢日志 ); // 设置日志器级别取所有sink中的最低级别这里debug是最低的 combined_logger-set_level(spdlog::level::debug); // 设置刷新级别ERROR及以上立即刷新保证错误日志不丢失 combined_logger-flush_on(spdlog::level::err); // 4. 注册为全局默认日志器可选方便使用SPDLOG_XXX宏 spdlog::set_default_logger(combined_logger); // 也注册到全局仓库方便通过名字获取 spdlog::register_logger(combined_logger); // 5. 设置全局异常处理器 spdlog::set_error_handler([](const std::string msg) { // 简单打印到标准错误避免使用可能出问题的日志系统 std::cerr “spdlog fatal error: ” msg std::endl; }); SPDLOG_INFO(“日志系统初始化完成。日志目录: {}” log_dir); return true; } catch (const std::exception ex) { std::cerr “初始化日志系统失败: ” ex.what() std::endl; return false; } } // 在main函数中使用 int main(int argc, char* argv[]) { if (!setup_logging(“my_awesome_app”, “/var/log/myapp”)) { return -1; } // 现在可以愉快地使用日志了 SPDLOG_DEBUG(“这是一条调试信息只出现在_all.log文件中。”); SPDLOG_WARN(“这是一条警告会出现在控制台和_all.log中。”); SPDLOG_ERROR(“这是一条错误会出现在控制台、_all.log和_error.log中。”); // ... 你的业务逻辑 // 程序正常退出前 spdlog::shutdown(); return 0; }这个配置提供了一个比较全面的起点控制台看关键错误全量日志用于深度调试错误日志单独文件方便对接监控告警系统。异步模式保证了性能阻塞策略保证了日志完整性。你可以根据自己项目的实际需求对这个配置进行增删改。