《C++ 工具库实战》高性能日志 spdlog:从基础使用到工程化封装

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux系统从入门到实践》《Linux网络从入门到实践》

《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

[一、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 库完成。 有两种情况区分:
  1. 系统包管理器安装的 spdlog,很多版本是外部 fmt ,fmt 单独打包,编译必须加上-lfmt;
  2. 源码编译 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
  • 业务意义: 线上项目常用这种方案,控制台只打印告警、错误方便运维看屏幕,而完整详细日志全部保存到文件,兼顾查看便捷性和日志完整性。
  • **注意:**每个 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 绑定、等级设置、格式设置。

程序启动时调用一次初始化接口,之后业务代码直接调用封装好的日志宏打印日志。

封装带来的好处总结:

  1. 日志自动携带文件名、行号,便于问题定位;
  2. 对外暴露简洁宏接口,业务代码不用重复写 spdlog 原生 API;
  3. 日志配置集中管理,切换控制台 / 文件输出、调整日志等级更加方便;

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++ 工程化内容,把命令行配置、单元测试与日志能力系统性地融入项目实践。

相关推荐
努力努力再努力wz1 小时前
【Lua入门系列】从 C++ 迭代器到 Lua 迭代器:一文深入理解 Lua 有状态以及无状态迭代器与泛型for、闭包、ipairs、pairs 和 next
开发语言·c++·lua
努力实干2 小时前
第二阶段 · 第 7 课:C++ 多线程与线程安全
开发语言·c++·安全
小菜要扣篮3 小时前
Ubuntu 下使用 vscode 打开 C++ 项目后,无法通过 F12 定位到定义问题解决
c++·vscode·ubuntu
wuminyu3 小时前
JVM规范学习总结:class执行性能差异
java·linux·c语言·jvm·c++
旖旎夜光3 小时前
LCR 170:交易逆序对的总数(归并排序) —— 题解
c++·学习·算法·leetcode·排序算法
C++ 老炮儿的技术栈3 小时前
Linux 动态库踩坑:明明重新编译了 .so,调用函数返回值却一直不变(libfjrobot.so 版本号异常排查实录)
linux·c语言·网络·c++·visual studio
dtq042412 小时前
C++小案例 - 日志实战
开发语言·c++
码匠许师傅13 小时前
【C++三方组件】Qt 概览:桌面 C++ 的事实标准与许可证三轨
开发语言·c++·qt
码匠许师傅14 小时前
【C++三方组件】POCO:跨平台系统编程全家桶
开发语言·c++