
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一、spdlog 介绍](#一、spdlog 介绍)
[1.1 spdlog 核心特性](#1.1 spdlog 核心特性)
[二、spdlog 安装](#二、spdlog 安装)
[三、spdlog 使用](#三、spdlog 使用)
[2.1 包含头文件](#2.1 包含头文件)
[2.2 日志输出等级枚举](#2.2 日志输出等级枚举)
[2.3 日志输出格式自定义](#2.3 日志输出格式自定义)
[2.4 日志记录器类 logger](#2.4 日志记录器类 logger)
[2.5 异步日志记录类 async_logger](#2.5 异步日志记录类 async_logger)
[2.6 日志记录器工厂](#2.6 日志记录器工厂)
[2.7 日志落地类 Sink](#2.7 日志落地类 Sink)
[2.8 全局配置接口](#2.8 全局配置接口)
[2.9 记录日志](#2.9 记录日志)
[4.1 案例一:彩色控制台标准输出](#4.1 案例一:彩色控制台标准输出)
[Makefile 编译命令](#Makefile 编译命令)
[4.2 案例二:普通文件输出](#4.2 案例二:普通文件输出)
[4.3 案例三:循环滚动文件输出](#4.3 案例三:循环滚动文件输出)
[4.4 案例四:multi_sink 多落地目标](#4.4 案例四:multi_sink 多落地目标)
[5.1 glog 简介](#5.1 glog 简介)
[5.1.1 性能](#5.1.1 性能)
[5.2 spdlog 简介](#5.2 spdlog 简介)
[5.2.1 性能](#5.2.1 性能)
[5.2.2 对比总结](#5.2.2 对比总结)
[六、spdlog 二次封装](#六、spdlog 二次封装)
[6.1 封装目的:获取代码文件名与行号](#6.1 封装目的:获取代码文件名与行号)
[6.1.1 定义全局日志器](#6.1.1 定义全局日志器)
[6.1.2 日志配置结构体](#6.1.2 日志配置结构体)
[6.1.3 全局日志初始化接口](#6.1.3 全局日志初始化接口)
[6.2 spdlog 封装代码](#6.2 spdlog 封装代码)
[6.2.1 头文件 Mylog.h](#6.2.1 头文件 Mylog.h)
[6.2.2 源文件 Mylog.cc](#6.2.2 源文件 Mylog.cc)
[6.2.3 测试主文件 log_test.cc](#6.2.3 测试主文件 log_test.cc)
[6.2.4 Makefile](#6.2.4 Makefile)
[6.2.5 log.conf 配置文件](#6.2.5 log.conf 配置文件)
[6.2.6 编译与运行](#6.2.6 编译与运行)
前言
在 C++ 项目开发中,日志是必不可少的基础能力,无论是程序状态跟踪、线上故障排查,还是问题复盘,都离不开日志系统的支撑。如果从零手写一套日志模块,需要处理线程安全、日志格式化、文件落盘、文件切割、异步写盘等大量繁琐细节,工程量与维护成本都很高。而 spdlog 作为一款成熟的高性能 C++ 日志库,可以帮我们快速补齐这些能力。
本文会从 spdlog 的特性与安装讲起,结合四个实战案例(彩色控制台输出、普通文件输出、滚动文件输出、multi_sink 多落地目标)逐一分析底层实现与用法,并与 glog 进行横向对比。在此基础上,我们会对 spdlog 做二次封装,让它能够自动携带文件名和行号,并借助 gflags 实现 "改配置文件即可切换日志策略" 的工程能力。
一、spdlog 介绍
spdlog 是一款面向C++ 的高性能日志库,主打极简上手与强悍的日志输出能力。在我们手写网络服务项目中,日志模块承担着状态打印、故障排查的作用,如果自己从零实现日志逻辑,不仅要处理线程安全、格式化、文件落盘等繁琐细节,性能也很难做到最优。而 spdlog 开箱即用,能够快速为项目接入完善的日志能力。
1.1 spdlog 核心特性
- **高性能:**库本身针对日志写入速度做了大量优化,高并发、大量日志打印场景下依旧可以维持不错性能,不会轻易拖慢业务主线程。
- **零配置:**属于头文件优先的日志库,不需要复杂的前期配置,引入头文件后就可以快速调用接口打印日志。
- **异步日志:**提供异步日志模式,日志消息先投递到队列,由单独线程负责落地,降低日志 IO 对业务线程的阻塞。
- **自定义格式化:**日志输出内容可以自由定制,能够按需携带时间戳、线程 ID、日志等级、源码行号等信息。
- **跨平台:**一套代码可运行在 Windows、Linux、macOS 等主流操作系统,跨平台项目迁移友好。
- **丰富 API:**内置多档日志级别,重载了多种输出运算符,原生支持各类数据类型打印,使用写法直观简洁。
二、spdlog 安装
Ubuntu 直接命令安装:
bash
sudo apt install libspdlog-dev
CentOS 直接命令安装:
CentOS 系统可以通过 yum 包管理器直接安装开发库:
bash
# CentOS7 / CentOS8
sudo yum install spdlog-devel
说明: CentOS 的包名后缀是
-devel,和 Ubuntu 的-dev命名习惯不一样。
源码编译安装(yum 版本过低时推荐)
bash
git clone https://github.com/gabime/spdlog.git
cd spdlog && mkdir build && cd build
cmake .. && make -j
make install
验证是否安装成功:
- 查看头文件
bash
ls /usr/include/spdlog
- 查看库文件
bash
# Ubuntu
ls /usr/lib/x86_64-linux-gnu/libspdlog*
# CentOS
ls /usr/lib64/libspdlog*
安装完成结果:


三、spdlog 使用
2.1 包含头文件
在 C++ 源码文件中引入 spdlog 核心头文件,后续所有日志相关接口都依赖该头文件。
cpp
#include <spdlog/spdlog.h>
2.2 日志输出等级枚举
spdlog 内置了一套日志级别,级别从低到高 依次排列,我们可以全局或者单独给 logger 设置日志过滤等级 。低于设置等级的日志会直接被忽略,不会输出。
cpp
namespace level {
enum level_enum : int {
trace = SPDLOG_LEVEL_TRACE, // 最细粒度追踪日志,用于代码流程跟踪
debug = SPDLOG_LEVEL_DEBUG, // 调试日志,开发阶段打印调试信息
info = SPDLOG_LEVEL_INFO, // 普通信息日志,打印程序正常运行状态
warn = SPDLOG_LEVEL_WARN, // 警告日志,非致命异常,但需要留意
err = SPDLOG_LEVEL_ERROR, // 错误日志,发生功能异常,但程序还能继续运行
critical = SPDLOG_LEVEL_CRITICAL, // 严重错误,一般会直接导致程序退出
off = SPDLOG_LEVEL_OFF, // 关闭所有日志输出
n_levels
};
}
2.3 日志输出格式自定义
我们可以调用set_pattern自定义日志输出模板,按需选择要打印的内容,例如时间、线程 ID、日志等级、日志正文等。
cpp
// 设置日志输出格式
logger->set_pattern("%Y-%m-%d %H:%M:%S [%t] [%-7l] %v");
一般对于 %l 来说我们会写成 %-7l,因为不同的日志等级显示的长度是不一致的,所以写成 %-7l 的目的就是统一格式方便我们观察日志内容。
格式占位符说明:
- % Y - 年
- % m - 月
- % d - 日
- % H - 小时(24 小时制)
- % M - 分钟
- % S - 秒
- % t - 线程 ID(Thread ID)
- % n - 日志器名称
- % l - 日志级别名称,如 INFO, DEBUG, ERROR
- % v - 日志内容
2.4 日志记录器类 logger
logger 是 spdlog 的核心类 ,我们所有日志打印操作 都由 logger 对象完成。
它封装了不同等级日志打印接口,同时支持修改日志格式、日志级别、刷新策略。
cpp
namespace spdlog {
class logger {
public:
// 构造函数:创建日志器,可以指定名称和输出目标sink
logger(std::string name);
logger(std::string name, sink_ptr single_sink);
logger(std::string name, sinks_init_list sinks);
void set_level(level::level_enum log_level); // 设置当前日志器的过滤等级
void set_formatter(std::unique_ptr<formatter> f); // 设置格式化器
void set_pattern(std::string pattern, pattern_time_type time_type); // 设置输出格式
// 不同级别日志打印接口,支持fmt格式化语法
template<typename... Args>
void trace(fmt::format_string<Args...> fmt, Args &&...args);
template<typename... Args>
void debug(fmt::format_string<Args...> fmt, Args &&...args);
template<typename... Args>
void info(fmt::format_string<Args...> fmt, Args &&...args);
template<typename... Args>
void warn(fmt::format_string<Args...> fmt, Args &&...args);
template<typename... Args>
void error(fmt::format_string<Args...> fmt, Args &&...args);
template<typename... Args>
void critical(fmt::format_string<Args...> fmt, Args &&...args);
void flush(); // 强制刷新缓冲区,把缓存日志写入输出目标
// 策略刷新:当触发指定等级日志时,自动刷新缓冲区
void flush_on(level::level_enum log_level);
};
}
// 使用示例:打印一条info日志
logger->debug("{} 的年龄是: {}", "xiaoming", 18);
2.5 异步日志记录类 async_logger
async_logger 继承自 logger ,接口使用方式和同步 logger 几乎一致。
区别在于同步日志直接在业务线程执行 IO 写操作;异步日志会把日志消息丢入队列,由独立线程池负责实际写入磁盘,减少 IO 阻塞对业务线程的影响。
cpp
// 异步日志器类,继承logger
class async_logger final : public logger {
async_logger(std::string logger_name,
sinks_init_list,
std::weak_ptr<details::thread_pool> tp,
async_overflow_policy overflow_policy = async_overflow_policy::block);
async_logger(std::string logger_name,
sink_ptr single_sink,
std::weak_ptr<details::thread_pool> tp,
async_overflow_policy overflow_policy = async_overflow_policy::block);
};
// 线程池类,异步日志底层依靠这个线程池消费日志队列
class SPDLOG_API thread_pool {
public:
thread_pool(size_t q_max_items,
size_t threads_n,
std::function<void()> on_thread_start,
std::function<void()> on_thread_stop);
thread_pool(size_t q_max_items, size_t threads_n);
};
// 获取全局线程池
std::shared_ptr<spdlog::details::thread_pool> thread_pool() {
return details::registry::instance().get_tp();
}
// 初始化全局异步线程池
inline void init_thread_pool(size_t q_size, size_t thread_count);
// 创建异步日志器示例
auto async_logger = spdlog::async_logger_mt("async_logger", "logs/async_log.txt");
async_logger->info("This is an asynchronous info message");
2.6 日志记录器工厂
spdlog 提供工厂函数 来快速创建 logger 对象 ,分为同步工厂、异步工厂。
内置多种创建接口,可以直接创建控制台输出、文件输出、滚动文件输出等 logger。
cpp
// 异步工厂别名
using async_factory = async_factory_impl<async_overflow_policy::block>;
// 工厂模板,用于创建异步logger
template<typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async(
std::string logger_name,
SinkArgs &&...sink_args)
// 创建彩色控制台输出logger(多线程安全)
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> stdout_color_mt(
const std::string &logger_name,
color_mode mode = color_mode::automatic);
// 创建普通控制台stderr日志器
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> stderr_color_mt(
const std::string &logger_name,
color_mode mode = color_mode::automatic);
// 创建普通文件日志器
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> basic_logger_mt(
const std::string &logger_name,
const filename_t &filename,
bool truncate = false,
const file_event_handlers &event_handlers = {});
// 创建滚动文件日志器:文件达到指定大小自动新建日志文件
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> rotating_logger_mt(
const std::string &logger_name,
const filename_t &filename,
size_t max_file_size,
size_t max_files,
bool rotate_on_open = false);
...
2.7 日志落地类 Sink
Sink 是日志最终输出的目标 ,一条日志经过 logger 处理后,交给 sink 完成输出。
spdlog 内置多种 Sink:控制台输出、普通文件、滚动文件,也支持扩展 kafka、tcp、udp 等自定义输出。_mt后缀代表多线程版本,线程安全,适合多线程项目。
cpp
namespace spdlog {
class SPDLOG_API sink {
public:
virtual ~sink() = default;
virtual void log(const details::log_msg &msg) = 0; // 输出日志消息的核心接口
virtual void flush() = 0; // 刷新缓冲区
virtual void set_pattern(const std::string &pattern) = 0;
virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) = 0;
virtual void set_level(level::level_enum log_level);
};
// 标准输出sink
using stdout_sink_mt;
using stdout_color_sink_mt;
using stderr_sink_mt;
using stderr_color_sink_mt;
// 滚动文件sink,文件写满自动拆分新日志文件
sink_ptr rotating_file_sink_mt(const filename_t base_filename,
std::size_t max_file_size,
std::size_t max_files,
bool rotate_on_open = false);
// 普通文件sink
sink_ptr basic_file_sink_mt(const filename_t &filename, bool truncate = false);
// 扩展sink,kafka、tcp、udp等
using kafka_sink_mt = kafka_sink<std::mutex>;
using tcp_sink_mt = tcp_sink<std::mutex>;
using udp_sink_mt = udp_sink<std::mutex>;
}

2.8 全局配置接口
spdlog 提供全局接口, 对所有 logger 统一生效,适合项目全局统一配置日志行为。
cpp
void set_level(level::level_enum log_level); // 全局设置日志过滤等级
void flush_every(std::chrono::seconds interval); // 定时自动刷新全局日志缓冲区
void flush_on(level::level_enum log_level); // 指定等级日志触发全局刷新
2.9 记录日志
拿到 logger 对象之后,直接调用对应等级接口打印日志,支持 fmt 风格格式化。
cpp
logger->trace("This is a trace message");
logger->debug("This is a debug message");
logger->info("This is a info message");
logger->warn("This is a warning message");
logger->error("This is an error message");
logger->critical("This is a critical message");
// spdlog 底层日志格式化依赖 fmt 库。
// spdlog 的info("num:{}", val)这种 {} 格式化语法,全部由 fmt 库完成。
// 在编译的时候就必须加上 -fmt 链接选项
四、完整示例讲解
4.1 案例一:彩色控制台标准输出
cpp
/*
标准输出的日志:
1. 包含头文件<spdlog/spdlog.h>
2.创建日志对象
3.设置日志输出等级
4.设置日志输出格式
5.输出日志
*/
#include <iostream>
#include <string>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h> // 彩色控制台sink头文件,stdout_color_mt 由此提供
// ===================== 案例一:彩色控制台标准输出 =====================
int main(int argc, char *argv[])
{
// 创建多线程安全的彩色控制台日志器,命名为stdout_logger
auto logger = spdlog::stdout_color_mt("stdout_logger");
// 设置日志输出等级为debug:只有debug及优先级更高的日志才会输出
// 日志级别优先级从小到大:trace < debug < info < warn < err < critical
// 如果等级设为info,trace、debug日志就会被直接丢弃,不会打印
logger->set_level(spdlog::level::debug);
// 自定义日志输出格式:[时:分:秒][日志级别]: 日志内容
logger->set_pattern("[%H:%M:%S][%^%l%$]: %v");
logger->debug("{}今年{}岁", "小明", 18);
logger->info("{}今年{}岁", "小红", 19);
logger->warn("{}今年{}岁", "小李", 20);
logger->error("{}今年{}岁", "小刚", 21);
logger->critical("{}今年{}岁", "小聪", 22);
return 0;
}
代码分析:
- **spdlog::stdout_color_mt("stdout_logger"):**创建多线程安全的彩色控制台日志器,_mt 代表 multi-thread。不同日志级别会自动带上不同颜色,方便在终端快速区分。
- logger->set_level(spdlog::level::debug): 日志过滤开关。 当前等级是debug,trace级别优先级比 debug 更低,trace 日志会直接被忽略,不会输出;debug、info、warn、error、critical 全部可以正常打印。 可以自行修改等级做对比测试,例如改为
spdlog::level::info,debug 日志就不再输出。 - set_pattern格式化字符串:%^%l%$ 是颜色标记,包裹%l日志级别名称,实现终端彩色效果。
- 打印接口使用 fmt 风格占位符
{},可以直接填入变量,不用像 printf 一样手动指定类型。
Makefile 编译命令
bash
stdout : stdout.cc
g++ $^ -o $@ -lspdlog -lpthread -lfmt
逐个链接选项分析:
-lspdlog
spdlog 库的本体。我们代码中调用的stdout_color_mt、logger、各种日志打印接口的函数实现 都在这个库里面。头文件<spdlog/spdlog.h>只提供函数声明,编译阶段只检查语法;链接阶段需要找到库里面的实现,因此必须手动链接-lspdlog,否则报 undefined reference。-lpthread
spdlog 大量用到 C++ 多线程:多线程安全的sink(_mt后缀)、异步日志 async_logger 内部线程池,底层都依赖 pthread 线程库。如果不添加这个链接选项,会找不到线程相关函数,触发链接报错。
**注意:**新版 g++ 虽然部分场景会自动携带 pthread,但 spdlog 并发相关逻辑强依赖线程库,显式加上更稳妥,跨平台、不同系统版本不会踩坑。
-lfmt
spdlog 底层日志格式化依赖 fmt 库 。spdlog 的info("num:{}", val)这种 {} 格式化语法,全部由 fmt 库完成。 有两种情况区分:
- 系统包管理器安装的 spdlog,很多版本是外部 fmt ,fmt 单独打包,编译必须加上
-lfmt; - 源码编译 spdlog 并开启内置 fmt(
SPDLOG_FMT_EXTERNAL=OFF),fmt 被编译进 spdlog 库,此时可以不用写-lfmt。
CentOS/Ubuntu 通过 yum/apt 安装的 spdlog-devel/libspdlog-dev,一般是外部 fmt,编译要追加
-lfmt。
运行结果展示:

4.2 案例二:普通文件输出
cpp
#include <iostream>
#include <string>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
// ===================== 案例二:普通文件输出 =====================
int main(int argc, char *argv[])
{
// 创建多线程安全文件日志器,名称为file_logger,日志写入 ./file.log
// basic_logger_mt 会打开指定文件,所有日志内容持久化写入磁盘文件
auto logger = spdlog::basic_logger_mt("file_logger", "./file.log");
// auto logger = spdlog::basic_logger_mt("file_logger", "./file.log", true);
// 设置日志输出等级为debug:只有debug及优先级更高的日志才会输出
// 日志级别优先级从小到大:trace < debug < info < warn < err < critical
// 如果等级设为info,trace、debug日志就会被直接丢弃,不会打印
logger->set_level(spdlog::level::debug);
// 自定义日志输出格式:[时:分:秒][日志级别]: 日志内容
// 注意:写入文件的日志没有终端,颜色标记 %^%$ 在文件中会变成乱码,生产写文件建议去掉颜色标记
logger->set_pattern("[%H:%M:%S][%-7l]: %v");
logger->debug("{}今年{}岁", "小明", 18);
logger->info("{}今年{}岁", "小红", 19);
logger->warn("{}今年{}岁", "小李", 20);
logger->error("{}今年{}岁", "小刚", 21);
logger->critical("{}今年{}岁", "小聪", 22);
return 0;
}
代码分析:
- **spdlog::basic_logger_mt("file_logger", "./file.log"):**创建多线程安全的文件日志器。basic代表普通文件 sink,日志持续追加写入同一个文件,不会自动分割文件。_mt表示多线程安全,多线程同时打日志不会出现内容错乱。
- set_level 日志过滤逻辑和控制台案例完全一致:当前等级 debug,trace 日志会被直接忽略,不会写入文件。修改级别可以过滤低优先级日志,减少磁盘 IO。
- 格式串要点:写入磁盘文件不要使用颜色占位符
%^ %$。颜色标记是终端 ANSI 转义码,写入文件会出现一堆不可见乱码字符,文件日志一般直接用%l打印级别名称。 - 运行程序后不会在终端打印内容,打开
file.log文件就可以看到全部日志内容。
注意: basic_file_sink 不会自动切割日志 ,文件会持续不断变大,适合日志量小的场景;日志量大 的场景要使用滚动文件 sink(案例三)。
运行结果展示:

4.3 案例三:循环滚动文件输出
cpp
#include <iostream>
#include <string>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/rotating_file_sink.h>
// ==================== 案例三:循环滚动文件输出 ====================
int main(int argc, char *argv[])
{
// 创建多线程安全滚动文件日志器
// 参数说明:日志器名称、基础文件名、单个文件最大字节数、最多保留文件个数
// 本例:单文件上限1024字节,最多保存3个日志文件,超出自动轮转
auto logger = spdlog::rotating_logger_mt("file_logger", "./rotating_file.log", 1024, 3);
// 设置日志输出等级为debug:只有debug及优先级更高的日志才会输出
// 日志级别优先级从小到大:trace < debug < info < warn < err < critical
// 如果等级设为info,trace、debug日志就会被直接丢弃,不会写入文件
logger->set_level(spdlog::level::debug);
// 自定义日志输出格式:[时:分:秒][日志级别]: 日志内容
// 文件日志不使用终端颜色标记,避免ANSI转义码造成文件乱码
logger->set_pattern("[%H:%M:%S][%l]: %v");
// 循环打印大量日志,用于触发文件滚动切割逻辑
for (int i = 0; i < 100; i++)
{
logger->info("hello world: {}", i);
}
return 0;
}
代码分析:
- spdlog::rotating_logger_mt("file_logger", "./rotating_file.log", 1024, 3)
滚动文件日志器,_mt保证多线程安全。
第三个参数1024:单个日志文件达到 1024 字节就触发切割;
第四个参数3:最多保留 3 份日志。当文件数量超过上限,最旧的日志文件会直接删除 ,循环复用 ,防止磁盘被日志占满 。
切割后生成文件命名:rotating_file.1.log、rotating_file.2.log、rotating_file.3.log。 - 日志等级过滤逻辑和前面案例保持一致。等级控制可以提前过滤低级别日志,减少磁盘写入压力。
- **与普通 basic_file_sink 对比:**basic 只会不断追加,文件无限变大;rotating 滚动 sink 适合线上生产环境,自动管控日志文件大小与数量,是项目里最常用方案。
- **for 循环作用:**一次性输出大量日志,快速填满文件,直观看到日志自动切割、旧日志淘汰的效果。
运行结果展示:

4.4 案例四:multi_sink 多落地目标
cpp
#include <iostream>
#include <string>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
// ===================== 案例四:multi_sink 多落地目标 =====================
void multi_sink_example()
{
// 创建彩色控制台sink(落地方向)对象,多线程安全
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
// 单独设置控制台输出等级:仅warn及更高级别的日志才会打印到终端
console_sink->set_level(spdlog::level::warn);
// 设置控制台日志格式,支持终端颜色
console_sink->set_pattern("[multi_sink_example] [%^%l%$] %v");
// 创建普通文件sink对象,第二个参数true代表日志追加写入文件
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("./multisink.log", true);
// 文件sink设置等级为trace:最低等级的日志都可以写入文件
file_sink->set_level(spdlog::level::trace);
// 构造logger,绑定多个sink:一份日志,可以同时输出到多个目标
spdlog::logger logger("multi_sink", {console_sink, file_sink});
// logger全局过滤等级:debug,只有>=debug的日志才会进入各个sink
// 过滤顺序:先经过logger总等级过滤,再分别经过每个sink自己的等级二次过滤
logger.set_level(spdlog::level::debug);
// 给logger设置全局日志格式(sink没有单独设置pattern时会使用这个)
logger.set_pattern("%Y-%m-%d %H:%M:%S [%l] %v");
// warn级别 >= console_sink(warn) 且 >= logger(debug),控制台+文件两边都会输出
logger.warn("this should appear in both console and file");
// info级别 >= logger(debug),但是低于console_sink设置的warn,因此仅写入文件,终端不打印
logger.info("this message should not appear in the console, only in the file");
}
int main(int argc, char *argv[])
{
multi_sink_example();
return 0;
}
代码分析:
- sink(落地器)概念:spdlog 中 sink 代表日志输出的目的地。multi-sink 就是一个日志器绑定多个输出目的地,一份日志同时分发到多个位置。
- 两级过滤机制(重点):
- 先经过logger 全局等级过滤 :低于 logger 等级的日志直接丢弃,不会下发给任何 sink。本案例 logger 等级是
debug,trace 日志直接被拦截。 - 通过 logger 校验后,再进入每个 sink 做独立二次等级过滤 。
- **file_sink 等级 trace:**接收 trace 及以上所有日志
- **console_sink 等级 warn:**只接收 warn、error、critical
- 先经过logger 全局等级过滤 :低于 logger 等级的日志直接丢弃,不会下发给任何 sink。本案例 logger 等级是
- 业务意义: 线上项目常用这种方案,控制台只打印告警、错误方便运维看屏幕,而完整详细日志全部保存到文件,兼顾查看便捷性和日志完整性。
- **注意:**每个 sink 可以独立配置日志格式、日志级别,互不干扰。
运行结果展示:

五、日志组件对比
glog 与 spdlog 都是 C++ 项目中广泛使用的开源日志库,二者设计思路不同,在性能、接口风格、功能特性上各有取舍,下面从特性、性能、适用场景展开对比。
5.1 glog 简介
glog 是谷歌推出的 C++ 日志库,具备完整的日志分级、条件日志、日志文件切割、信号捕获、自定义格式等能力。 glog 默认采用同步写盘模式,调用日志接口时,会阻塞当前线程,直到日志数据写入磁盘。
5.1.1 性能
在同步调用场景下,glog 性能弱于 spdlog。参考性能测试:在低配服务器上,处理十万条日志,glog 耗时 1.027 秒;固态硬盘环境耗时 0.475 秒。同步写盘会带来明显的线程阻塞,高并发大量打日志时容易拖累业务主线程。
5.2 spdlog 简介
spdlog 是高性能 C++ 日志库,核心亮点是支持异步日志,日志落地操作交由后台线程处理,不会阻塞业务主线程。使用门槛低,开箱即用,仅引入头文件就能快速上手,支持多输出目标、自定义格式、原生线程安全。
5.2.1 性能
相同测试条件下,spdlog 同步版本速度优于 glog。低配服务器十万条日志耗时 0.135 秒,固态硬盘环境仅 0.057 秒。 额外提供异步模式,异步模式十万条日志耗时 0.158 秒;高并发场景,异步模式可以极大降低日志 IO 对业务代码的影响。
5.2.2 对比总结
- 性能:同步场景 spdlog 速度明显高于 glog。日志量大的服务,spdlog 吞吐优势突出。
- 异步日志:spdlog 原生支持异步日志,高负载服务推荐开启,避免主线程被磁盘 IO 阻塞;glog 原生没有异步能力。
- 易用性:spdlog 集成简单,头文件即可使用;glog 编译、配置步骤相对繁琐。
- 功能:glog 独有条件日志、信号处理等特色能力,适合依赖这类特性的项目。
- 使用场景:glog 适合日志量不大、需要特殊日志能力、性能压力不高的场景;spdlog 适合高性能后端,需要异步日志、高吞吐日志输出的程序。
**选型建议:**如果业务并发高、日志量大,不希望日志 IO 阻塞业务逻辑,优先 spdlog;已有项目在用 glog,性能压力不大,或者强依赖 glog 独有的条件日志、信号处理特性,可以继续使用 glog。
六、spdlog 二次封装
原生 spdlog 库虽然功能齐全,但直接在业务代码中裸用会存在一些不便之处。工程开发中,我们一般会在 spdlog 基础上做一层二次封装,来补齐业务需要的能力。下面介绍封装的设计目标。
6.1 封装目的:获取代码文件名与行号
原生 spdlog 调用日志接口时,最大的不便之处就是在于:不会自动打印日志调用点的文件名和行号 。排查线上 bug 时,我们需要快速定位这条日志出自代码哪一行,就需要借助编译器内置宏 __FILE__(当前文件名) 、__LINE__(当前代码行号)。
- 原生写法:
cpp
logger->error("hello world - {}", i);
只能输出日志文本,看不到代码位置。
- 手动携带文件行号写法:
cpp
logger->error("[{}:{}]: hello world-{}", __FILE__, __LINE__, i);
每次打日志都手动传入__FILE__、__LINE__非常繁琐,容易遗漏。 我们可以通过宏封装,定义 ERR(fmt, ...) 这类日志宏,自动带上文件与行号,简化调用。
cpp
#define ERR(fmt, ...) logger->error("[{}:{}]: " fmt, __FILE__, __LINE__, ##__VA_ARGS__)
##VA_ARGS 是 GCC 扩展,可变参数为空的时候,自动去掉前面逗号,避免编译报错。
6.1.1 定义全局日志器
每次使用都手动创建、保存 logger 对象比较麻烦。封装时创建一个全局 logger 实例,项目所有文件可以直接使用同一个日志对象,不用反复创建与传递 logger 指针。
6.1.2 日志配置结构体
定义一个结构体 统一管理日志全部配置项,集中管理:
- 日志输出等级 level
- 日志输出格式 pattern
- 日志器类型(控制台 / 普通文件 / 滚动文件)
- 输出目标(sink,可以是控制台、文件或者多 sink 组合) 后续修改日志策略,只需要修改结构体配置,不需要改动多处业务代码。
6.1.3 全局日志初始化接口
提供统一的初始化函数,传入上面的配置结构体,一次性完成 logger 创建、sink 绑定、等级设置、格式设置。
程序启动时调用一次初始化接口,之后业务代码直接调用封装好的日志宏打印日志。
封装带来的好处总结:
- 日志自动携带文件名、行号,便于问题定位;
- 对外暴露简洁宏接口,业务代码不用重复写 spdlog 原生 API;
- 日志配置集中管理,切换控制台 / 文件输出、调整日志等级更加方便;
6.2 spdlog 封装代码
6.2.1 头文件 Mylog.h
cpp
/*
日志操作封装:
1.防止头文件重复包含
2.包含头文件
3.声明命名空间
4.声明全局日志器
5.声明日志配置结构体
6.声明全局日志器初始化接口
7.封装日志输出宏
*/
// 防止头文件重复包含
#ifndef __MYLOG_H__
#define __MYLOG_H__
// 包含头文件
#include <iostream>
#include <string>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/async.h>
// 声明命名空间
// 自定义日志命名空间,避免命名污染
namespace Mylog
{
// 声明全局日志器,extern代表变量定义在cc源文件中
extern std::shared_ptr<spdlog::logger> g_logger;
// 声明日志配置结构体
struct log_settings
{
bool async; // 是否启用异步日志:true->启用;false->不启用
int level; // 日志输出等级:debug-1;info-2;warn-3;err-4;off-6
std::string format; // 日志输出格式:[%H:%M:%S][%-7l] [%t]-%v
std::string path; // 日志输出目标:stdout/file
};
// 声明全局日志器初始化接口
extern void InitLog(const log_settings &settings);
// 日志前缀,用于拼接文件名行号
#define FMT_PREFIX std::string("[{}:{}]: ")
// //封装日志输出宏(%v)
// #define DEBUG(fmt, ...) g_logger->debug(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
// #define INFO(fmt, ...) g_logger->info(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
// #define WARN(fmt, ...) g_logger->warn(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
// #define ERR(fmt, ...) g_logger->error(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
// 封装日志输出宏(%v)
// ##__VA_ARGS__ GCC扩展,可变参数为空时自动消除前面逗号,防止编译报错
#define DEBUG(fmt, ...) Mylog::g_logger->debug(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#define INFO(fmt, ...) Mylog::g_logger->info(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#define WARN(fmt, ...) Mylog::g_logger->warn(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#define ERR(fmt, ...) Mylog::g_logger->error(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
// 在外部使用该宏时,在编译的时候会展开成后面的部分,
// 如果是上面的写法直接g_logger调用函数,外部会不认识g_logger这个变量,因为这个变量是在当前命名空间域中声明定义的
// 所以需要Mylog进行访问
}
#endif
头文件分析:
- 使用命名空间
Mylog隔离代码,防止和项目其他名字冲突。 log_settings统一收纳日志全部配置,后续可以通过配置 / 命令行修改参数,不用修改业务代码。- 日志宏
DEBUG/INFO/WARN/ERR是封装的核心,自动带入__FILE__文件名、__LINE__行号,解决原生 spdlog 无法自动打印代码位置的痛点。 extern std::shared_ptr<spdlog::logger> g_logger:仅声明,实例定义放在 Mylog.cc 中,保证全局唯一日志器。
6.2.2 源文件 Mylog.cc
cpp
#include "Mylog.h"
namespace Mylog
{
// 定义全局日志器
std::shared_ptr<spdlog::logger> g_logger;
// 定义全局日志器初始化接口
void InitLog(const log_settings &settings)
{
// 1、判断日志类型 - async/sync
// 2、判断输出目标 - stdout/file
// 3、创建日志器
if (settings.async)
{
// 说明日志类型为异步
if (settings.path == "stdout")
{
// 日志输出目标为stdout_color_mt
g_logger = spdlog::stdout_color_mt<spdlog::async_factory>("stdout_logger");
}
else
{
// 日志输出目标为stdout_c
g_logger = spdlog::basic_logger_mt<spdlog::async_factory>("file_logger", "./file.log");
}
}
else
{
// 说明日志类型为同步
if (settings.path == "stdout")
{
// 日志输出目标为stdout_color_mt
g_logger = spdlog::stdout_color_mt("stdout_logger");
}
else
{
// 日志输出目标为stdout_c
g_logger = spdlog::basic_logger_mt("file_logger", "./file.log");
}
}
// 4、将配置里的数字等级转为spdlog枚举,设置日志过滤等级
g_logger->set_level(spdlog::level::level_enum(settings.level));
// 、设置日志格式
g_logger->set_pattern(settings.format);
}
}
Mylog.cc 代码分析:
- 定义全局
g_logger,项目中所有文件都可以通过头文件访问这个日志器。 InitLog是统一入口,根据settings.async区分同步 / 异步模式 ,settings.path区分控制台输出还是文件输出。- 异步模式使用
spdlog::async_factory,日志写入任务交给后台线程,不阻塞业务主线程;同步模式日志写 IO 在调用线程执行。 - 初始化最后统一设置日志等级与格式,全部配置由外部传入,底层 spdlog 创建逻辑对业务透明。
**注意:**当前代码只支持 basic 普通文件,如需滚动文件,可以在这个分支继续拓展 rotating_file_sink。
6.2.3 测试主文件 log_test.cc
cpp
#include "Mylog.h"
#include <gflags/gflags.h>
using namespace Mylog;
// 使用gflags定义命令行参数,运行程序时可动态修改日志配置
DEFINE_bool(log_async, true, "是否启用异步日志");
DEFINE_int32(log_level, 1, "日志输出等级: debug-1;info-2;warn-3;err-4;off-6");
DEFINE_string(log_format, "[%H:%M:%S][%-7l] [%t]-%v", "日志输出格式");
DEFINE_string(log_path, "stdout", "日志输出目标: stdout/file");
int main(int argc, char *argv[])
{
// 解析命令行参数,gflags核心接口
google::ParseCommandLineFlags(&argc, &argv, true);
// 将gflags解析得到的命令行参数填入日志配置结构体
log_settings settings = {
.async = FLAGS_log_async,
.level = FLAGS_log_level,
.format = FLAGS_log_format,
.path = FLAGS_log_path
};
// 调用封装好的初始化函数,完成日志器创建
InitLog(settings);
// 使用封装好的日志宏打印日志,自动携带文件名和行号
DEBUG("{}今年{}岁", "小明", 18);
INFO("{}今年{}岁", "小红", 19);
WARN("{}今年{}岁", "小李", 20);
ERR("{}今年{}岁", "小刚", 21);
}
测试代码分析:
- 结合前面学习的 gflags,将日志所有配置暴露为命令行参数,不用修改代码、不用重新编译,启动程序时动态切换日志策略。
- 执行流程: 解析命令行参数 → 填充 log_settings 配置结构体 → 调用
InitLog初始化全局日志器 → 使用宏打印日志。 - 业务层调用非常简洁,只需要
DEBUG/INFO/WARN/ERR宏,自动带上文件行号,不用每次手动传入__FILE__和__LINE__。
6.2.4 Makefile
bash
# 头文件搜索路径,去../source目录寻找头文件
CFLAGS=-I../source
log_test:log_test.cc ../source/Mylog.cc
g++ -o $@ $^ -std=c++17 $(CFLAGS) -lpthread -lgflags -lspdlog -lfmt
- -I../source: 指定头文件检索目录,编译器找到 Mylog.h。
- 链接库说明:
- **
-lpthread:**多线程库,spdlog 异步模式依赖线程; - **
-lgflags:**链接 gflags 库; - **
-lspdlog:**链接 spdlog 日志库; - **
-lfmt:**fmt 格式化库,spdlog 底层依赖。
- **
- 编译命令: make
运行示例:./log_test --log_async=false --log_path=stdout,动态修改日志配置。
6.2.5 log.conf 配置文件
在上面的编译命令我们会发现,当我们 gflags 参数多起来的时候,如果我们需要手动对每个参数都进行设置,命令行运行程序就会非常繁琐,所以针对 gflags 参数设置相对应的配置文件是需要有的。
bash
-log_async=true
-log_level=1
-log_format=[%H:%M:%S][%-7l] [%t]-%v
-log_path=file
# 注意:配置文件内的参数名必须和代码 DEFINE 定义的名字完全一致,字符串值不需要加双引号
核心价值: 不用每次启动程序写一长串命令行参数。把所有日志配置写在
log.conf里,修改配置只改文件,不用改代码、不用重新编译,非常适合线上部署。
6.2.6 编译与运行
gflags 内置--flagfile参数,可以读取文件内所有参数。
bash
# 在example目录执行,加载log.conf配置文件
./log_test --flagfile=log.conf
#或者不加--flagfile=log.conf选项,那么就是使用默认参数
./log_test
- 执行这条命令后,gflags 会读取
log.conf里全部配置项,自动覆盖默认参数。 - 优先级:命令行直接传入参数 > flagfile 配置文件 > 代码 DEFINE 里设置的默认值。
比如:./log_test --flagfile=log.conf --log_level=3,则 log_level=3 会覆盖配置文件里的 1。
运行结果展示:

拓展: 如果需要线上动态调整,运维只需要修改 log.conf,重启程序即可切换日志等级、同步 / 异步、控制台 / 文件输出,业务代码完全不用改动。
结束语
本篇《C++ 工具库实战》spdlog 高性能日志库详解,从库的定位、特性与安装讲起,通过彩色控制台、普通文件、滚动文件、multi_sink 四个实战案例掌握了 spdlog 的核心用法,并与 glog 做了横向对比,最后完成了对 spdlog 的二次封装,配合 gflags 实现了 "配置文件驱动日志策略" 的工程方案。
日志系统的核心价值,在于用最轻量的成本记录关键运行信息,辅助定位与排查问题。spdlog 的异步日志、多 sink、日志等级过滤、滚动切割等能力,覆盖了从开发调试到线上生产的完整诉求。在此基础上做二次封装,自动携带文件名与行号、集中管理配置、屏蔽底层 API 差异,让业务代码写日志更简洁、更可控,也让日志策略可以随环境灵活调整,无需反复修改代码。
GTest、gflags 与 spdlog,都是 C++ 后端开发中非常实用的基础工具库。掌握它们并合理运用,能让工程代码更规范、更健壮、更容易维护。后续我们可以继续探索更多 C++ 工程化内容,把命令行配置、单元测试与日志能力系统性地融入项目实践。