在C++项目中实现稳定可维护的日志记录,spdlog是目前最受欢迎的轻量级开源方案之一。它全部由头文件组成,无需编译额外库,支持同步与异步模式、多种输出目标以及丰富的格式化能力。无论是小型工具还是高并发服务端,都能通过简单接口快速集成。

一、spdlog基础与环境准备
spdlog是一个只有头文件的C++日志库,兼容C++11及以上标准。由于不需要链接二进制库,我们只需要把它的include/spdlog目录拷贝到工程里,或者在编译时通过-I指定头文件路径即可。这种设计让它在跨平台和嵌入式场景中非常友好,不会因为依赖链复杂而拖慢构建。
在使用前,建议先确认编译器支持C++11,因为spdlog内部大量使用了std::move、lambda和线程库。如果是CMake项目,可以直接通过FetchContent拉取源码,也可以把仓库作为子模块引入。下面是一段最基础的引入方式,不需要任何初始化就能输出日志。
#include <spdlog/spdlog.h>
int main() {
// 直接使用内置的默认控制台日志器
spdlog::info("系统启动完成,版本号 {}", "1.0.2");
spdlog::warn("内存占用偏高:{} MB", 512);
spdlog::error("数据库连接失败,错误码 {}", 1045);
return 0;
}
上面代码中的spdlog::info、warn、error都是全局默认日志器提供的快捷方法。它们会自动带上时间戳并输出到控制台。虽然方便,但在真实项目中我们通常要创建自己的日志器,以便控制输出文件和格式。
二、创建文件与控制台复合日志器
实际开发中,我们往往希望日志既打印到屏幕方便调试,又写入文件便于事后排查。spdlog通过sink概念来描述输出目标,可以组合多个sink到一个logger中。以下示例创建一个同时输出到控制台和按大小滚动文件的日志器。
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <memory>
int main() {
// 控制台彩色输出sink
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::debug);
// 文件滚动sink,最大5MB,保留3个备份
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"logs/app.log", 5 * 1024 * 1024, 3);
file_sink->set_level(spdlog::level::info);
// 组合两个sink
std::vector<spdlog::sink_ptr> sinks = {console_sink, file_sink};
auto logger = std::make_shared<spdlog::logger>("multi", sinks.begin(), sinks.end());
logger->set_level(spdlog::level::debug);
logger->flush_on(spdlog::level::warn);
spdlog::register_logger(logger);
logger->info("复合日志器初始化成功");
logger->debug("这条只在控制台显示");
return 0;
}
在这个例子中,set_level为每个sink单独设定了级别,因此debug信息不会进文件,只出现在控制台;而warn及以上级别会触发立即刷盘,避免程序崩溃时丢失关键日志。这种细粒度控制是spdlog的一大优势。
另外需要注意,如果程序是长时间运行的服务,应当捕获std::signal信号并在退出前调用spdlog::shutdown(),确保缓冲区中的日志被写入磁盘。否则异步或带缓存的sink可能丢最后几条记录。
三、自定义日志格式与异步模式
spdlog允许通过set_pattern定制每一行日志的样式。常见的字段包括时间、线程号、日志级别和消息体。下面展示一个包含日期、毫秒和所在文件行的格式:
#include <spdlog/spdlog.h>
int main() {
auto logger = spdlog::default_logger();
// 格式:时间.毫秒 [线程号] 级别 消息
logger->set_pattern("%Y-%m-%d %H:%M:%S.%e [%t] [%^%l%$] %v");
logger->info("自定义格式生效");
return 0;
}
当日志量非常大时,同步写文件可能阻塞业务线程。spdlog提供异步日志器,背后有一个独立线程负责消费队列并写入sink。创建方式如下:
#include <spdlog/async.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
int main() {
// 初始化异步线程池,队列8192,1个工作线程
spdlog::init_thread_pool(8192, 1);
auto async_file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("async.log");
auto logger = std::make_shared<spdlog::async_logger>(
"async", async_file, spdlog::thread_pool());
spdlog::register_logger(logger);
for (int i = 0; i < 1000; ++i) {
logger->info("异步日志条目 {}", i);
}
spdlog::shutdown();
return 0;
}
异步模式能显著降低IO等待,但队列满时默认会阻塞调用方。如果对实时性要求极高且允许丢日志,可改用async_overflow_policy::overrun_oldest策略。此外,异步日志器必须在程序结束前调用spdlog::shutdown(),否则后台线程可能被强制终止,导致日志缺失。
四、常见误区与性能建议
很多初学者会把spdlog的宏级别判断忽略掉,在热路径里直接拼接字符串。虽然spdlog接收的是格式化参数,只有在真正输出时才会构造字符串,但如果传入的是昂贵的计算结果,仍会产生开销。建议使用SPDLOG_DEBUG之类的宏,在编译期就剔除不需要的日志。
#include <spdlog/spdlog.h>
void process() {
// 仅当debug开启时才计算耗时操作
SPDLOG_DEBUG("当前处理包大小 {}", expensive_compute());
}
另一个容易忽视的点是日志级别动态调整。在线上环境,我们可以通过信号或管理接口调用logger->set_level()临时调低级别排查问题,事后再恢复,而不需要重启进程。结合文件滚动和异步写入,spdlog足以支撑绝大多数C++服务的日志需求。
总体来看,spdlog以极低的接入成本提供了企业级日志能力。只要理解sink、logger和线程池的协作关系,就能在项目中写出清晰、高效且易于维护的日志记录代码。