C++高性能日志库选型指南:spdlog、glog与easylogging++对比与实践 1. 项目概述为什么我们需要一个“好”的日志库干了这么多年C开发我敢说日志是项目里最容易被轻视却又最能在关键时刻“救命”的模块。想想那些场景线上服务半夜崩了你只能对着一个空荡荡的控制台或者一个早已被撑爆的、毫无结构的文本文件发呆多线程程序出现诡异的数据竞争没有时间戳和线程ID你根本无从下手性能瓶颈若隐若现你却无法在不影响主流程的情况下低开销地记录关键路径的执行时间。这些痛我都经历过。所以当项目从“玩具”走向“产品”选择一个成熟、可靠、高效的日志库绝不是锦上添花而是雪中送炭。今天要聊的就是C生态里那些久经沙场的经典轻量级开源日志库。所谓“轻量级”并不意味着功能简陋而是指它们设计精巧、依赖少、集成简单同时在高性能、多线程、易用性上做到了极佳的平衡。它们就像是瑞士军刀体积小但功能全能帮你快速构建起一套强大的日志基础设施让你能把精力集中在业务逻辑本身而不是反复造一个脆弱的轮子。无论是开发一个高性能服务器还是一个桌面应用程序一个好的日志库都是你代码健壮性的基石。2. 核心需求解析一个好日志库的自我修养在深入具体库之前我们必须先达成共识一个好的C日志库应该满足哪些核心需求这决定了我们的选型标准。2.1 性能与开销速度是硬道理日志操作本身是I/O密集型任务尤其是文件写入。一个糟糕的日志实现可能会成为系统的性能瓶颈。因此高性能是首要考量。这主要体现在两个方面一是同步日志的调用开销要极低确保即使在不开启日志时日志语句对程序的影响也微乎其微二是要提供异步日志能力。异步日志意味着日志消息被放入一个缓冲区由后台线程负责批量写入文件或其他输出端。这能将日志I/O的延迟与主业务线程解耦避免因为磁盘速度慢而阻塞关键业务流程。对于高频日志场景如金融交易、游戏服务器异步日志几乎是必选项。2.2 线程安全性多线程世界的基石现代C程序几乎都是多线程的。日志库必须保证在多线程同时调用日志接口时不会出现数据竞争、消息交错或崩溃。这要求库内部对共享资源如缓冲区、文件句柄进行恰当的锁保护或无锁设计。一个线程安全的日志库你可以在任何线程、任何时刻放心地调用它而不用担心日志内容错乱或程序崩溃。2.3 灵活的日志级别与输出控制日志不能只有“开”和“关”。我们需要精细的控制。通常的日志级别包括TRACE/DEBUG: 最详细的调试信息用于开发阶段追踪程序流。INFO: 常规的运行信息如服务启动、配置加载。WARN: 警告信息表明可能有问题但不影响核心功能。ERROR: 错误信息表明某个操作失败但程序可能还能运行。CRITICAL/FATAL: 致命错误通常会导致程序终止。日志库应允许在运行时动态调整日志级别。例如在生产环境我们只输出INFO及以上级别而在排查问题时可以临时开启DEBUG级别而无需重启服务。2.4 多样化的输出目标Sinks日志不能只打印到控制台。一个成熟的日志库应支持多种输出目标Sinks控制台带颜色高亮最佳滚动文件按大小或时间自动分割、归档日志文件防止单个文件过大。系统日志如Syslog on Linux, Event Log on Windows网络发送到远程日志服务器如Logstash, Fluentd。自定义目标允许用户自定义Sink将日志发送到数据库、消息队列等。2.5 易用性与可读性API设计要直观、符合现代C习惯比如支持流式输出或格式化字符串。日志输出的格式应该清晰易读通常包括时间戳最好精确到微秒、日志级别、线程ID、源代码文件名和行号、以及用户消息。这些信息对于问题定位至关重要。2.6 可配置性与低耦合日志库的配置如输出格式、文件路径、级别最好能通过配置文件如JSON, YAML或代码API方便地设置。同时日志库应该与你的业务代码低耦合方便在项目后期替换或升级。3. 主流轻量级日志库横向对比与选型了解了核心需求我们来看看C社区里几个最受推崇的轻量级日志库。它们各有侧重适合不同的场景。3.1 spdlog速度与功能的完美结合spdlog可能是目前C社区最热门、口碑最好的日志库。它的名字就揭示了其特点Speed Log。核心优势极致的性能spdlog在设计上极度追求性能。其同步日志器速度极快而异步日志模式更是其王牌功能。它使用了一个先进先出的环形缓冲区后台线程批量写入对前端线程的延迟影响几乎可以忽略不计。在我的一个高频交易模拟项目中切换到spdlog异步日志后日志带来的尾延迟下降了90%以上。丰富的特性支持所有主流功能多线程、多日志级别、滚动文件、控制台彩色输出、自定义格式化。它的格式化语法类似Python的str.format()或C20的std::format非常强大易用。头文件库整个库由头文件组成只需包含头文件即可使用无需编译链接库文件集成成本为零。这对于小型项目或快速原型开发来说非常友好。活跃的社区更新频繁文档齐全Issue响应快。适用场景几乎适用于所有需要高性能日志的C项目特别是服务器后端、高频计算、游戏引擎等对性能敏感的场景。如果你不确定选哪个spdlog通常是安全且优秀的第一选择。简单示例#include “spdlog/spdlog.h” #include “spdlog/sinks/rotating_file_sink.h” int main() { // 创建一个按文件大小滚动的日志器单个文件最大5MB保留3个备份 auto file_logger spdlog::rotating_logger_mt(“my_logger”, “logs/mylog.txt”, 1048576 * 5, 3); spdlog::set_default_logger(file_logger); spdlog::set_level(spdlog::level::debug); // 设置全局日志级别 spdlog::info(“欢迎使用spdlog”); spdlog::error(“发生了一个错误错误码{}”, 42); spdlog::warn(“注意温度过高当前值{:.2f}”, 36.5); // 格式化浮点数保留两位小数 // 异步日志示例通常用于性能要求更高的场景 auto async_file spdlog::basic_logger_mtspdlog::async_factory(“async_logger”, “logs/async.txt”); async_file-info(“这是一条异步日志消息”); }3.2 glogGoogle风格的稳健之选glog(Google Logging Library) 是Google出品的老牌日志库以其稳定性和强大的故障处理能力著称。核心优势条件日志与检查宏这是glog的一大特色。除了常规的LOG(INFO) “msg”它提供了CHECK宏如CHECK_EQ(a, b)在条件失败时会记录FATAL错误并终止程序同时打印出丰富的调试信息如栈轨迹。这对于在开发早期捕获致命假设错误非常有用。信号处理与崩溃转储glog内置了对一些致命信号如SIGSEGV的处理可以在程序崩溃时自动将最后的日志信息刷新到磁盘并尝试生成堆栈跟踪极大方便了事后调试。日志目的地管理可以通过环境变量GLOG_log_dir等非常灵活地控制日志输出文件的位置和前缀对于分布式系统部署很友好。分级别、分模块输出可以将不同严重级别的日志输出到不同的文件如INFO日志到programname.INFOERROR日志到programname.ERROR。需要注意的点它不是纯头文件库需要编译和链接。其API风格是Google C风格使用流式操作符与现代的格式化字符串风格不同。默认配置下功能全面但可能略显“重”定制化不如spdlog灵活。适用场景适合大型项目、基础服务尤其是需要强健的故障检查和崩溃现场保存能力的场景。如果你喜欢或习惯了Google内部工具链的风格glog会非常顺手。简单示例#include glog/logging.h int main(int argc, char* argv[]) { google::InitGoogleLogging(argv[0]); // 初始化参数通常为程序名 google::SetLogDestination(google::INFO, “./myapp.INFO.”); // 设置INFO级别日志文件前缀 FLAGS_logtostderr false; // 不输出到stderr FLAGS_alsologtostderr false; // 也不额外复制到stderr LOG(INFO) “程序启动”; int x 10; int y 20; CHECK_EQ(x * 2, y) “基本的算术检查失败”; // 如果x*2不等于y程序会终止并记录FATAL日志 for (int i 0; i 100; i) { LOG_EVERY_N(INFO, 10) “每10次迭代记录一次这是第 “ google::COUNTER “ 次”; } LOG(ERROR) “模拟一个错误”; google::ShutdownGoogleLogging(); // 清理 return 0; }3.3 easylogging功能全面的单头文件库easylogging是一个功能异常丰富的单头文件日志库。正如其名它旨在让日志记录变得极其容易。核心优势功能极其全面它可能是功能最全的单头文件日志库。支持配置化通过代码或配置文件、性能追踪、条件/每N次日志、日志回滚、多平台、自定义格式等你能想到的几乎所有功能。强大的配置能力支持通过配置文件进行详细配置可以在不重新编译的情况下改变日志行为。性能追踪可以非常方便地记录函数或代码块的执行时间对于性能剖析很有帮助。使用简便通过几个宏即可使用学习曲线平缓。需要注意的点因为所有功能都塞进了一个头文件导致这个头文件非常大超过万行可能会增加编译时间。虽然功能多但在极端性能要求下其异步日志等核心性能可能不如专门优化的spdlog。由于其宏定义非常复杂在某些复杂的编译环境下可能会与其他库的宏产生冲突。适用场景适合需要快速集成一个功能全面、开箱即用日志库的中小型项目特别是那些不希望处理编译依赖、又需要高级功能如性能追踪的场景。简单示例// 只需包含一个头文件 #include “easylogging.h” // 初始化宏只需要在一个cpp文件中使用 INITIALIZE_EASYLOGGINGPP int main(int argc, char* argv[]) { // 从配置文件加载配置 el::Configurations conf(“my_log.conf”); el::Loggers::reconfigureAllLoggers(conf); // 使用宏记录日志 LOG(INFO) “这是一条INFO日志”; LOG(ERROR) “这是一条ERROR日志错误码” errno; // 性能追踪示例 TIMED_FUNC(timerObj); // 开始追踪整个函数的耗时 { TIMED_SCOPE(blockTimer, “mySlowBlock”); // 追踪某个代码块的耗时 // … 一些耗时的操作 … } // 函数结束时会自动在日志中输出耗时信息 return 0; }选型小结追求极致性能和现代API首选spdlog。需要强健的检查断言和崩溃处理用于大型系统考虑glog。希望一个头文件解决所有问题需要性能追踪等高级功能试试easylogging。对于超轻量级、仅需最基本功能还可以考虑NanoLog无锁设计性能极高但功能少或plog跨平台小巧。4. 实战以spdlog为例构建企业级日志模块理论说再多不如动手搭一个。我们以spdlog为例演示如何构建一个适合中型项目的、功能完备的日志模块。这个模块将包含同步/异步日志器、滚动文件、控制台彩色输出、全局访问接口和简单的配置化。4.1 环境准备与集成首先将spdlog集成到你的项目中。最推荐的方式是使用包管理器如vcpkg, conan或CMake的FetchContent。使用CMake FetchContent推荐无需手动下载# 在你的CMakeLists.txt中添加 include(FetchContent) FetchContent_Declare( spdlog GIT_REPOSITORY https://github.com/gabime/spdlog.git GIT_TAG v1.x # 指定一个稳定版本如v1.13.0 ) FetchContent_MakeAvailable(spdlog) # 然后你的目标链接spdlog target_link_libraries(your_target PRIVATE spdlog::spdlog)这种方式能自动处理依赖和编译选项是最省心的。4.2 设计一个封装类直接使用全局的spdlog::info()等函数虽然方便但不利于统一管理和配置。我们设计一个Logger单例类。// Logger.h #pragma once #include memory #include spdlog/spdlog.h #include spdlog/async.h // 异步日志需要 #include spdlog/sinks/rotating_file_sink.h #include spdlog/sinks/stdout_color_sinks.h class Logger { public: static Logger getInstance(); void init(bool async false, const std::string log_dir “./logs”); // 提供获取原始spdlog日志器指针的接口用于更高级的定制 std::shared_ptrspdlog::logger getLogger() const { return logger_; } // 也可以直接提供便捷的日志函数可选 templatetypename… Args void trace(const char* fmt, const Args… args) { logger_-trace(fmt, args…); } // … 类似地实现debug, info, warn, error, critical … private: Logger() default; ~Logger(); std::shared_ptrspdlog::logger logger_; static const size_t ASYNC_QUEUE_SIZE 8192; // 异步队列大小 };4.3 实现核心初始化逻辑初始化函数init是核心它负责创建sink输出目标并组合成日志器。// Logger.cpp #include “Logger.h” #include filesystem // C17用于创建目录 namespace fs std::filesystem; Logger Logger::getInstance() { static Logger instance; return instance; } void Logger::init(bool async, const std::string log_dir) { if (logger_) { spdlog::warn(“Logger already initialized!”); return; } // 1. 创建日志目录如果不存在 fs::path dir_path(log_dir); if (!fs::exists(dir_path)) { if (!fs::create_directories(dir_path)) { throw std::runtime_error(“Failed to create log directory: “ log_dir); } } // 2. 创建多个sink std::vectorspdlog::sink_ptr sinks; // 控制台sink带颜色 auto console_sink std::make_sharedspdlog::sinks::stdout_color_sink_mt(); console_sink-set_level(spdlog::level::info); // 控制台只输出info及以上级别 console_sink-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%t] %v”); sinks.push_back(console_sink); // 滚动文件sink std::string file_path (dir_path / “app.log”).string(); auto file_sink std::make_sharedspdlog::sinks::rotating_file_sink_mt( file_path, 1024 * 1024 * 10, 5); // 单个文件10MB保留5个备份 file_sink-set_level(spdlog::level::debug); // 文件记录更详细的debug级别 file_sink-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] [%s:%#] %v”); sinks.push_back(file_sink); // 3. 创建日志器 if (async) { // 异步日志器 spdlog::init_thread_pool(ASYNC_QUEUE_SIZE, 1); // 队列大小线程数 logger_ std::make_sharedspdlog::async_logger( “async_logger”, sinks.begin(), sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block // 队列满时阻塞防止丢日志 ); } else { // 同步日志器 logger_ std::make_sharedspdlog::logger(“sync_logger”, sinks.begin(), sinks.end()); } // 4. 设置全局日志级别和刷新级别 logger_-set_level(spdlog::level::debug); logger_-flush_on(spdlog::level::warn); // 遇到warn及以上级别日志时立即刷新到磁盘 // 5. 注册为全局日志器可选这样可以直接使用spdlog::info()等 spdlog::set_default_logger(logger_); spdlog::info(“Logger initialized successfully. Mode: {}”, async ? “Async” : “Sync”); } Logger::~Logger() { if (logger_) { spdlog::info(“Shutting down logger…”); logger_-flush(); spdlog::drop_all(); // 清理所有注册的日志器 } }关键点解析set_pattern定义了日志的输出格式。%Y-%m-%d %H:%M:%S.%e是带微秒的时间戳%^%l%$是带颜色的级别%t是线程ID%s:%#是源文件名和行号%v是用户消息。文件日志包含了文件名和行号便于定位控制台日志则用颜色高亮级别便于肉眼识别。flush_on设置触发立即刷新的日志级别。对于ERROR或FATAL日志我们希望立刻写入磁盘防止程序崩溃导致最后的错误信息丢失。这里设置为warn是一个比较保守且安全的选择。异步策略spdlog::async_overflow_policy::block意味着当异步队列满时前端日志调用会被阻塞直到队列有空间。这保证了日志不丢失但可能影响性能。另一种策略是overrun_oldest它会丢弃最老的日志适合对性能要求极高且可以容忍少量日志丢失的场景。生产环境通常建议使用block。4.4 在项目中使用在main函数或程序初始化入口处初始化日志器然后就可以在全局任意地方使用了。// main.cpp #include “Logger.h” #include thread void workerThread(int id) { for (int i 0; i 3; i) { // 使用封装的接口 Logger::getInstance().getLogger()-info(“Thread {}: Task {} started”, id, i); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 或者使用spdlog的全局函数因为我们已经set_default_logger spdlog::debug(“Thread {}: Task {} detail data: {}”, id, i, i * 3.14); } } int main() { try { // 初始化日志器异步模式日志目录为./logs Logger::getInstance().init(true, “./logs”); spdlog::info(“Main application started.”); // 模拟多线程日志 std::vectorstd::thread threads; for (int i 0; i 4; i) { threads.emplace_back(workerThread, i); } for (auto t : threads) { t.join(); } spdlog::warn(“模拟一个警告信息此条日志会触发flush。”); spdlog::error(“模拟一个错误信息”); spdlog::info(“Main application exiting.”); } catch (const std::exception e) { // 初始化失败或其他异常尝试使用最基本的stderr输出 fprintf(stderr, “Fatal error during initialization: %s\n”, e.what()); return 1; } return 0; }运行后你会在控制台看到彩色输出同时在./logs目录下生成类似app.log、app.log.1等的滚动日志文件。5. 高级配置与性能调优实战基础功能搭建好了但要用于生产环境还需要一些“打磨”。5.1 日志格式深度定制spdlog的格式模式非常强大。除了上面用到的基本元素还有一些有用的占位符%n: 日志器名称。%P: 进程ID。%T: 制表符。%: 源文件名和行号短格式。%!: 函数名需要编译器支持如GCC/Clang的__FUNCTION__。你可以根据团队习惯定制格式。例如一个更详细的格式// 包含进程ID、更完整的函数名和源代码位置 file_sink-set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%P:%t] [%l] [%s:%# %!] %v”);5.2 异步日志的精细控制异步日志的性能和稳定性取决于几个关键参数队列大小 (ASYNC_QUEUE_SIZE)队列是预分配的内存块。大小需要权衡。太小如1024在高并发下容易导致生产者业务线程阻塞太大如65536会占用更多内存。对于大多数应用8192到32768是一个合理的范围。你可以通过监控日志队列的待处理消息数来调整。后台线程数spdlog::init_thread_pool(queue_size, thread_count)。通常1个后台线程就足够了因为I/O操作最终是串行的一个文件。多个线程主要用于将日志分发给多个不同的sink如同时写文件和网络。对于单一文件sink多线程不会带来收益反而可能增加锁开销。刷新策略除了flush_on还可以手动调用logger-flush()或者在程序退出前确保所有日志都被刷新。我们的封装类在析构函数中做了这个工作。5.3 日志级别动态调整在生产环境我们可能需要在运行时调整日志级别比如临时打开DEBUG日志来排查问题而不重启服务。spdlog本身不直接提供HTTP接口但我们可以结合一个简单的信号处理或内置管理接口来实现。一个简单的思路是用一个全局原子变量存储当前级别并定期检查一个配置文件或者监听一个特定的信号如SIGUSR1。// 简化示例通过信号切换级别 #include csignal std::atomicspdlog::level::level_enum g_log_level{spdlog::level::info}; void signal_handler(int signal) { if (signal SIGUSR1) { // 收到SIGUSR1信号在INFO和DEBUG之间切换 auto current g_log_level.load(); auto new_level (current spdlog::level::info) ? spdlog::level::debug : spdlog::level::info; g_log_level.store(new_level); spdlog::set_level(new_level); spdlog::info(“Log level switched to {}”, spdlog::level::to_string_view(new_level)); } } int main() { std::signal(SIGUSR1, signal_handler); // … 初始化日志器 … spdlog::set_level(g_log_level.load()); // … 主循环 … }在Linux上你可以通过kill -USR1 pid来动态调整该进程的日志级别。5.4 日志文件管理与清理滚动日志能防止单个文件过大但时间久了还是会积累很多日志文件。我们需要一个清理策略。spdlog的滚动文件sink只负责按大小或日期创建新文件不负责删除旧文件。清理旧日志需要额外实现。一个常见的策略是在程序启动时或定时任务中检查日志目录删除超过N天的日志文件。#include chrono #include filesystem namespace fs std::filesystem; void cleanup_old_logs(const std::string log_dir, int keep_days) { try { auto now fs::file_time_type::clock::now(); for (const auto entry : fs::directory_iterator(log_dir)) { if (entry.is_regular_file() entry.path().extension() “.log”) { auto ftime fs::last_write_time(entry); auto age std::chrono::duration_caststd::chrono::hours(now - ftime); if (age std::chrono::hours(24 * keep_days)) { fs::remove(entry.path()); spdlog::info(“Deleted old log file: {}”, entry.path().string()); } } } } catch (const fs::filesystem_error e) { spdlog::error(“Failed to clean up log files: {}”, e.what()); } } // 可以在Logger::init中调用或由外部定时器触发6. 常见问题排查与性能陷阱即使使用了优秀的库在实际部署中还是会遇到各种问题。这里记录几个我踩过的坑和解决方案。6.1 日志性能导致服务延迟飙升现象服务监控显示在请求高峰期服务的P99延迟异常增高但CPU和内存使用率并不高。排查使用性能分析工具如perf发现大量时间花在了fwrite或锁竞争上。根因与解决同步日志I/O阻塞这是最常见的原因。在高并发下每个日志调用都直接写文件磁盘I/O成为瓶颈。解决务必启用异步日志。切换到异步模式后前端线程只需将日志放入内存队列延迟立刻大幅下降。日志格式过于复杂或单条日志太大频繁调用std::string格式化、或拼接非常大的字符串如记录整个数据包即使异步也会消耗大量CPU在格式化上。解决优化日志内容避免在热路径上记录过大的对象。使用更高效的格式化方式spdlog的fmt库性能已经很好但要避免在日志语句中进行复杂的计算或函数调用。异步队列设置不当队列大小太小导致生产者频繁阻塞。解决适当增大队列大小如从8192调整到32768并监控队列使用情况。spdlog提供了logger-overflow_count()可以查看因队列满而丢弃的日志数如果使用overrun_oldest策略。6.2 程序崩溃时最后的日志丢失现象程序段错误崩溃但打开日志文件发现崩溃前几秒的ERROR日志没有。根因日志还停留在应用缓冲区或库的缓冲区中没来得及写入磁盘进程就结束了。解决设置合理的flush_on级别如我们之前做的flush_on(spdlog::level::warn)让WARN及以上级别的日志立即触发刷盘。对于致命错误这很关键。程序正常退出时手动刷新在main函数返回前或信号处理函数中调用spdlog::shutdown()或logger-flush()。我们的封装类在析构函数中做了这个操作但要注意析构函数的调用顺序确保日志器在其他全局/静态对象销毁之前刷新。考虑使用更可靠的sink对于极其关键的日志可以考虑使用spdlog::sinks::basic_file_sink_mt非缓冲模式但性能差或者自定义一个sink每条日志都调用fflush不推荐性能损失大。6.3 多线程日志内容错乱或程序卡死现象日志行中夹杂着乱码或者不同线程的日志内容交织在一起更严重时程序死锁。根因日志库本身非线程安全如果你错误地使用了一个非线程安全的日志库或配置就会出现数据竞争。解决确保使用的sink是线程安全的版本spdlog中通常以_mt结尾如stdout_color_sink_mt。在信号处理函数中调用非异步信号安全的函数在信号处理函数如SIGSEGV处理函数中直接调用spdlog::info()是危险的因为malloc、printf等函数可能不是异步信号安全的可能导致死锁。解决信号处理函数中只做最简单的操作如设置一个原子标志位。在主线程或专门的日志线程中检查这个标志位并记录日志。glog的崩溃处理内部使用了更安全的机制这也是它的优势之一。6.4 日志文件占用磁盘空间增长过快现象磁盘空间报警发现是日志目录占用了大量空间。根因与解决日志级别过低在生产环境还开着DEBUG或TRACE级别。解决确保生产环境日志级别至少为INFO。通过环境变量或配置文件管理级别。滚动文件大小设置过大或备份文件过多例如单个文件设置成100MB保留10个备份那就是1GB。解决根据实际需求调整。通常单个文件10-50MB保留5-10个备份是合理的。结合上面提到的定期清理策略删除超过7天或30天的旧日志文件。日志内容过于冗余每条日志都记录了太多不必要的信息。解决审查日志语句移除不必要的重复信息或过于详细的数据转储。遵循“足够定位问题即可”的原则。6.5 第三方库也使用了相同的日志库导致冲突现象你的项目使用了spdlog你链接的某个第三方库也静态链接了spdlog可能导致单例冲突或链接错误。解决统一版本和链接方式尽可能让整个项目使用相同版本的spdlog并且都使用动态链接或头文件模式。使用命名空间隔离如果第三方库强制使用了某个版本你可以考虑将你自己使用的spdlog包装在一个独立的命名空间中但这通常需要修改spdlog源码比较麻烦。使用外部日志接口定义一个项目内部统一的日志抽象接口然后提供spdlog的实现。这样底层日志库的更换不会影响上层业务代码也便于与第三方库的日志进行整合或转发。日志模块是系统的“黑匣子”其稳定性和可靠性直接决定了线上问题排查的效率。投入时间选择一个合适的库并正确配置在项目生命周期中绝对是回报率最高的投资之一。从spdlog开始根据你的项目规模和对特定功能如CHECK断言、崩溃处理的偏好选择最适合你的那把“瑞士军刀”然后忘掉日志的烦恼专注于创造业务价值吧。