【即时通讯系统】环境搭建1——gflags,spdlog

目录

一.gflags的安装使用

1.1.安装gflags

1.2.使用gflags

1.2.1.宏的定义语法

1.2.2.示例1------通过命令行修改变量

1.2.3.示例2------通过配置文件修改变量

二.spdlog的安装使用

1.1.安装spdlog

1.2.使用spdlog

1.2.1.日志等级

1.2.2.日志输出格式自定义

1.2.3.日志记录器类1------默认的logger类

1.2.4.日志记录器类2------自定义日志记录器类

1.2.5.spdlog日志记录器工厂类

1.2.6.全局接口

1.2.7.多Sink日志示例

1.2.8.异步日志示例

1.3.封装spdlog进行二次封装


一.gflags的安装使用

1.1.安装gflags

gflags安装的过程其实很简单

bash 复制代码
sudo apt-get install libgflags-dev

1.2.使用gflags

gflags是干啥的

gflags 是 Google 开发的一个开源库,**用于 C++ 应用程序中命令行参数的声明、定义 和解析。**gflags 库提供了一种简单的方式来添加、解析和文档化命令行标志(flags), 使得程序可以根据不同的运行时配置进行调整。

想象一下,你写了一个非常厉害的程序。这个程序在运行时,经常需要调整一些"开关"或"参数",比如:

  • --port 8080:服务要监听哪个端口?
  • --max_users 1000:最多允许多少人同时在线?
  • --log_dir ./logs:程序运行的日志存到哪个文件夹?
  • --enable_debug:要不要打开调试模式?

这些参数如果直接写死在程序代码里,每次想改(比如把端口从8080改成9090),你就得去翻源代码、重新编译,非常麻烦,在正式服务器上更是不可能这么做。

gflags 就是专门为了解决这个问题而生的一个C++库。它的核心作用,就是帮你优雅、统一地管理这些程序通过命令行来运行时参数。

你可以把它理解为一个智能、统一的"程序启动参数控制板"。它的工作原理和好处是:

1. 集中定义,随处可用

你在代码里一个专门的地方,用简单的宏定义好所有可能的参数(叫什么名字、是什么类型、默认值是多少、简单的说明文字)。

之后,在整个程序的任何一个角落,你都可以像使用普通变量一样来读取这些参数的值。gflags帮你做好了全局访问。

2. 多种方式灵活设置

程序启动时,你可以通过多种方式来设定这些参数的值,优先级通常从高到低:

  • 命令行 (最常用):启动程序时直接加参数,比如 ./my_program --port=9090 --log_dir=/var/log

  • 配置文件 :把参数写在一个文件里,启动时用 --flagfile=my.conf 来指定。

  • 环境变量:通过特定的环境变量来设置。

  • 代码里直接改(不推荐,仅用于测试)。

3. 自动帮你的忙

  • 类型检查 :你定义--port是整数,如果用户输入了一个字符串,gflags会报错,而不是让程序崩溃。

  • 自动生成"--help" :你只要定义参数时写了说明文字,程序运行时加上 --help-help,gflags就会自动打印出一份漂亮、完整的参数使用说明,不需要你手动写。

  • 支持复杂类型:除了基本的整数、布尔值、字符串,还支持列表等。

我知道,如果我们只是单纯的去讲解它的功能,很难理解,我们必须去用一用它。才能更好的理解。

1.2.1.宏的定义语法

gflags 提供了一系列宏来定义不同类型的命令行参数:

  1. 布尔类型
bash 复制代码
DEFINE_bool(参数名, 默认值, "帮助信息");

示例

cpp 复制代码
DEFINE_bool(enable_debug, false, "是否启用调试模式");
// 使用:--enable_debug 或 --enable_debug=true
// 关闭:--noenable_debug 或 --enable_debug=false
  1. 整数类型
cpp 复制代码
DEFINE_int32(参数名, 默认值, "帮助信息");     // 32位整数
DEFINE_int64(参数名, 默认值, "帮助信息");     // 64位整数
DEFINE_uint32(参数名, 默认值, "帮助信息");    // 无符号32位
DEFINE_uint64(参数名, 默认值, "帮助信息");    // 无符号64位

示例

cpp 复制代码
DEFINE_int32(server_port, 8080, "服务器监听端口");
DEFINE_int32(max_connections, 1000, "最大连接数");
// 使用:--server_port=9090 --max_connections=5000
  1. 浮点数类型
cpp 复制代码
DEFINE_double(参数名, 默认值, "帮助信息");

示例

cpp 复制代码
DEFINE_double(threshold, 0.85, "匹配阈值");
// 使用:--threshold=0.92
  1. 字符串类型
cpp 复制代码
DEFINE_string(参数名, 默认值, "帮助信息");

示例

cpp 复制代码
DEFINE_string(config_file, "config.ini", "配置文件路径");
DEFINE_string(log_dir, "/var/log/myapp", "日志目录");
// 使用:--config_file=production.conf --log_dir=/tmp/logs

1.2.2.示例1------通过命令行修改变量

我们直接编写一个main.cpp

cpp 复制代码
// 最简单gflags例子 - 打招呼程序
#include <iostream>
#include <gflags/gflags.h>

// 定义参数:就定义在 main 文件里,简单!
DEFINE_string(name, "小明", "要打招呼的人名");  // 名字,默认是"小明"
DEFINE_bool(loud, false, "是否大声打招呼");     // 是否大声,默认false

int main(int argc, char** argv) {
    // 1. 解析命令行参数
    google::ParseCommandLineFlags(&argc, &argv, true);
    
    // 2. 打印结果
    std::cout << "你好,";
    
    // 使用 FLAGS_name 获取名字参数的值
    std::cout << FLAGS_name;
    std::cout << "!";
    
    // 根据 loud 参数决定是否加感叹号
    if (FLAGS_loud) {
        std::cout << "!!!" << std::endl;
    } else {
        std::cout << "。" << std::endl;
    }
    
    return 0;
}

大家注意:FLAGS_name和FLAGS_loud的由来:

在gflags中,当我们使用 DEFINE_xxx 宏定义参数时,宏会自动帮我们生成一个全局变量,这个变量的名字就是 FLAGS_ 加上参数名。

  • 所以,DEFINE_string(name, ...) 会自动生成一个全局变量 FLAGS_name,类型是std::string。
  • 同样,DEFINE_bool(loud, ...) 会自动生成一个全局变量 FLAGS_loud,类型是bool。
cpp 复制代码
g++ main.cpp -o test -lgflags

怎么样?我们居然可以通过命令行参数来设置里面的变量的值。是不是很神奇!!

......

1.2.3.示例2------通过配置文件修改变量

main.cpp

cpp 复制代码
// main.cc - 演示gflags基本用法
#include <gflags/gflags.h>
#include <iostream>

// 定义命令行参数
DEFINE_bool(reuse_addr, true, "是否开启网络地址重用选项");
DEFINE_int32(log_level, 1, "日志等级:1-DEBUG, 2-WARN, 3-ERROR");
DEFINE_string(log_file, "stdout", "日志输出位置设置,默认为标准输出");

int main(int argc, char* argv[]) 
{
    // 解析命令行参数
    google::ParseCommandLineFlags(&argc, &argv, true);
    
    // 输出参数值
    std::cout << "reuse_addr: " << FLAGS_reuse_addr << std::endl;
    std::cout << "log_level: " << FLAGS_log_level << std::endl;
    std::cout << "log_file: " << FLAGS_log_file << std::endl;
    
    return 0;
}

配置文件编写:main.conf

bash 复制代码
# main.conf - gflags配置文件
# 注意:配置文件中使用一个减号(-)而不是两个(--)
-reuse_addr=true
-log_level=3
-log_file=./log/main.log

makefile

cpp 复制代码
main : main.cpp
	g++ -std=c++17 $^ -o $@ -lgflags 

测试1:查看帮助信息

......

测试2:使用默认参数运行

测试3. 通过命令行传递参数

测试4. 通过配置文件传递参数

测试5:参数优先级命令行参数会覆盖配置文件中的设置

现在是不是就很明白了。这个gflags的作用就很好理解了

二.spdlog的安装使用

1.1.安装spdlog

这个的安装其实是非常简单的

此外,我们需要注意:这个spdlog依赖于fmt库进行格式化输出。所以我们还需要安装fmt库

cpp 复制代码
sudo apt-get install libspdlog-dev &&\
sudo apt-get install libfmt-dev

1.2.使用spdlog

spdlog干啥用的

  • 第一部分:什么是日志?为什么需要它?

想象一下,你写的程序就像一个复杂的机器人在执行任务。当它在运行时,你怎么知道:

  • 它现在正在做什么?("开始连接数据库...")

  • 它遇到了什么问题?("错误:网络连接失败!")

  • 它的性能怎么样?("处理1000条数据耗时2.3秒")

  • 程序崩溃了,崩溃前最后一刻发生了什么?

你不可能一直盯着屏幕看。这时,就需要日志 功能。它让程序在执行过程中,自动把关键信息、状态、警告和错误 记录下来,并输出到某个地方(比如屏幕上的控制台、或者一个文件里),供你事后查看和分析。这对调试程序、监控运行状态、分析线上问题至关重要。

  • 第二部分:spdlog 是什么?它的核心优势

你可以把 spdlog 理解为专门为 C++ 程序设计的、一个现成的、高质量的"记录员"。你不需要从零开始自己写这个记录员,只需要"雇佣"它,并告诉它怎么记录就行。

它的核心优势就是你提到的:

  1. 高性能与超快速:这个记录员手速极快,记录信息时几乎不影响机器人(你的主程序)干活。尤其在"异步模式"下(后面会讲),它更是快到飞起。

  2. 零配置/易用 :就像你买了一个智能设备"开箱即用"一样,只需要包含头文件就可以使用,日志功能就启动了,非常简单。

  3. 功能丰富:这个记录员不仅会写字,还自带高级功能:能记录不同级别的事件、能自动加上时间和线程编号、能把记录同时写到好几个地方(如屏幕和文件)。

话不多说,我们直接先看一个例子

main.cpp

cpp 复制代码
#include <iostream>
#include "spdlog/spdlog.h"  // 包含spdlog头文件

int main() {
    // 1. 最简单的用法 - 使用默认的日志记录器
    spdlog::info("这是一个信息日志");
    
    // 2. 不同类型的日志
    spdlog::warn("这是一个警告日志");
    spdlog::error("这是一个错误日志");
    
    // 3. 带格式化的日志
    spdlog::info("用户{}登录成功,年龄{}", "张三", 25);
    
    // 4. 设置日志级别(只显示错误和更严重的日志)
    spdlog::set_level(spdlog::level::err);
    spdlog::info("这条不会显示");  // 不会显示,因为级别太低
    spdlog::error("这条会显示");   // 会显示
    
    return 0;
}

编译

cpp 复制代码
g++ main.cpp -o test -lspdlog -lfmt

1.2.1.日志等级

我们的spdlog定义了8种日志级别:

cpp 复制代码
namespace level {
    enum level_enum : int {
        trace = SPDLOG_LEVEL_TRACE,    // 0
        debug = SPDLOG_LEVEL_DEBUG,    // 1
        info = SPDLOG_LEVEL_INFO,      // 2
        warn = SPDLOG_LEVEL_WARN,      // 3
        err = SPDLOG_LEVEL_ERROR,      // 4
        critical = SPDLOG_LEVEL_CRITICAL, // 5
        off = SPDLOG_LEVEL_OFF,        // 6
        n_levels                       // 7 - 等级数量
    };
}
  1. trace(追踪级别)

**这是最低级别的日志,**用于输出最详细的程序执行信息。就像是程序的"显微镜",记录每一个微小的步骤和状态变化。当需要详细了解程序每一步如何执行时使用这个级别。由于信息量极大,通常只在开发复杂的算法或排查极其隐蔽的问题时开启,生产环境中几乎从不使用。

  1. debug(调试级别)

专门为调试程序而设计的级别。它记录开发者在调试过程中需要查看的各种中间状态、变量值和流程信息。就像程序的"日记本",详细记录了程序做了什么事情、怎么做的。这个级别通常只在开发和测试阶段使用,帮助开发者定位问题。

  1. info(信息级别)

**这是最常用的级别,用于记录程序正常运行的关键信息。**它就像是程序的"健康报告",告诉用户程序正在做什么重要的事情、运行状态如何。比如服务器启动成功、用户完成某个重要操作等。生产环境通常会保持这个级别开启,用于监控系统运行状态。

  1. warn(警告级别)

表示程序中出现了一些异常情况,但这些问题还不至于影响核心功能。就像汽车的"警示灯",提醒驾驶员注意某些潜在风险。这些信息需要关注但不需要立即处理,比如磁盘空间不足、网络连接偶尔失败等。

  1. err(错误级别)

表示程序中发生了真正的错误,导致某些功能无法正常工作。这就像是程序的"故障报告",指出出现了需要修复的问题。比如文件打开失败、数据库连接断开、业务逻辑错误等。这些错误通常需要人工介入处理。

  1. critical(严重级别)

最高级别的错误,表示程序遇到了可能导致崩溃或系统不可用的严重问题。这就像是程序的"急救信号",需要立即处理。比如内存耗尽、关键数据损坏、系统资源枯竭等。这类日志通常会触发警报机制。

  1. off(关闭级别)

这不是一个真正的日志级别,而是一个"开关"。当设置为这个值时,所有日志输出都会被关闭,程序将不记录任何日志。通常用于完全不需要日志的场景,或者在特殊调试时需要临时关闭所有日志。

  1. n_levels(级别数量)

这个值不是一个日志级别,而是一个技术性的枚举值,表示一共有多少个级别定义。它的值是7,对应着从trace到off的7个不同定义。在编程中,有时会用它来遍历所有级别或进行边界检查。


我们可以通过设置日志等级

main.cpp

cpp 复制代码
#include "spdlog/spdlog.h"

int main() {
    // 设置日志级别 - 只显示该级别及以上的日志
    spdlog::set_level(spdlog::level::info);
    
    // 这些会显示(因为级别 >= info)
    spdlog::info("程序启动");      // 显示
    spdlog::warn("内存不足");      // 显示
    spdlog::error("文件错误");     // 显示
    
    // 这些不会显示(因为级别 < info)
    spdlog::trace("详细跟踪");     // 不显示
    spdlog::debug("调试信息");     // 不显示
    
    // 动态改变日志级别
    spdlog::set_level(spdlog::level::debug);
    spdlog::debug("现在debug可以显示了");  // 显示
    
    // 检查当前级别
    auto current_level = spdlog::get_level();
    spdlog::info("当前日志级别:{}", 
        spdlog::level::to_string_view(current_level));
    
    return 0;
}

我们编译运行

cpp 复制代码
g++ main.cpp -o test -lspdlog -lfmt

怎么样?是不是理解了?

1.2.2.日志输出格式自定义

核心概念:格式占位符

spdlog 使用**% 符号加上一个字母**来表示不同的日志信息部分。就像填空题一样,每个占位符会在输出时被对应的实际值替换。

基本占位符讲解

  1. 时间日期类

这些占位符会被替换为具体的时间值:

  • %Y → 年份,如:2024
  • %m → 月份,01到12
  • %d → 日期,01到31
  • %H → 小时(24小时制),00到23
  • %M → 分钟,00到59
  • %S → 秒,00到59

时间组合示例:

  • %Y-%m-%d %H:%M:%S → 2024-01-15 14:30:25
  • %m/%d %H:%M → 01/15 14:30
  1. 日志内容类
  • %v →这是最重要的,就是你写的日志消息内容
  • %l → 日志级别,比如 INFO、ERROR、DEBUG
  • %t → 线程ID,每个线程有不同的数字
  • %n → 日志器名称,如果你给日志器起了名字

对齐和宽度控制语法

格式:% + 对齐符号 + 宽度数字 + 占位符字母

    1. 左对齐:%-数字

%-7l → 日志级别左对齐,占7个字符宽度

示例:

INFO → "INFO "(INFO后加3个空格)

ERROR → "ERROR "(ERROR后加2个空格)

WARNING → "WARNING"(正好7个字符)

    1. 右对齐:%数字

%7l → 日志级别右对齐,占7个字符宽度

示例:

INFO → " INFO"(前面加3个空格)

ERROR → " ERROR"(前面加2个空格)

为什么需要对齐?

为了让日志在终端或文件中看起来整齐,所有级别名称占用同样宽度,便于阅读。

示例

示例1

cpp 复制代码
#include <spdlog/spdlog.h>

int main() {
    // 设置自定义日志格式
    spdlog::set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v");
    
    // 输出不同级别的日志
    spdlog::info("这是一条信息日志");
    spdlog::warn("这是一条警告日志");
    spdlog::error("这是一条错误日志");
    
    // 使用对齐功能
    spdlog::set_pattern("[%Y-%m-%d %H:%M:%S] [%-7l] %v");
    
    spdlog::info("使用左对齐的日志");
    spdlog::warn("看到级别名称对齐了吗?");
    spdlog::error("错误级别也占7个字符宽度");
    
    // 添加线程ID
    spdlog::set_pattern("[%Y-%m-%d %H:%M:%S] [%-7l] [thread:%t] %v");
    
    spdlog::info("这条日志包含线程ID");
    
    return 0;
}

我们进行编译

cpp 复制代码
g++ main.cpp -o test -lspdlog -lfmt

实例2

cpp 复制代码
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <thread>
#include <vector>
#include<iostream>

int main() {
    // 示例1:基础格式设置
    std::cout << "=== 示例1:基础格式 ===" << std::endl;
    spdlog::set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v");
    
    spdlog::info("用户登录成功");
    spdlog::warn("内存使用率超过80%");
    spdlog::error("数据库连接失败");
    
    // 示例2:对齐和宽度控制
    std::cout << "\n=== 示例2:对齐控制 ===" << std::endl;
    spdlog::set_pattern("[%H:%M:%S] [%-8l] [%-6t] %v");
    
    spdlog::info("开始处理请求");
    spdlog::debug("调试信息");
    spdlog::error("文件不存在");
    
    // 示例3:多线程场景
    std::cout << "\n=== 示例3:多线程日志 ===" << std::endl;
    spdlog::set_pattern("[%H:%M:%S] [thread:%-5t] [%-5l] %v");
    
    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back([i] {
            spdlog::info("线程 {} 开始执行", i);
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            spdlog::info("线程 {} 执行完成", i);
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

我们进行编译

cpp 复制代码
g++ main.cpp -o test -lspdlog -lfmt

1.2.3.日志记录器类1------默认的logger类

这个日志记录器类------logger类就是用来记录我们的日志的。

有人就好奇了,我上面不是已经通过类似spdlog::info,spdlog::warn等全局函数打印出了日志了吗?为什么还需要来搞一个日志记录器类?

就像:spdlog::info("用户登录成功");

spdlog::warn("内存使用率超过80%");

事实上,我们上面可以通过全局函数来打印日志,其实还是因为我们的spdlog内部自带有一个默认的,全局的logger类,我们通过spdlog::info等全局函数来打印日志,其实都是调用了这个全局的logger类内部的方法来调用,包括我们使用这个spdlog::set_pattern来设置这个日志输出格式,也都是在修改这个默认的logger类内部的设置。

当然,这个默认的logger类也不是一开始就存在的,他也是有创建时机的。

那什么时候会创建默认的logger类?

cpp 复制代码
// 当第一次调用以下任何函数时,会自动创建默认logger:
spdlog::info("Hello");      // 自动创建默认logger
// 或者
spdlog::default_logger();   // 自动创建默认logger

// 创建的是:控制台输出、带颜色、名为""

注意这里创建的是spdlog 库默认的、全局共享的 logger 实例

❌ 错误理解(以为会构造新的)

cpp 复制代码
// 很多人会误以为这样创建了新的logger
auto logger = spdlog::default_logger();  // ❌ 以为这里创建了新对象

// 然后再次调用,以为会有两个不同的logger
auto logger2 = spdlog::default_logger();  // ❌ 以为这是第二个logger

✅ 正确理解(获取同一个实例)

cpp 复制代码
// 实际上,这两个获取的是同一个logger实例
auto logger1 = spdlog::default_logger();  // 获取默认logger
auto logger2 = spdlog::default_logger();  // 获取的是同一个logger

// 验证:修改一个会影响另一个
logger1->set_pattern("[简单格式] %v");
// 现在logger2的输出格式也变了,因为它们是同一个对象

实际上,整个程序只有一个默认logger,所有全局函数调用都使用同一个默认logger实例。

我们直接看例子

实例

示例1

cpp 复制代码
#include <spdlog/spdlog.h>
#include <iostream>

int main() {
    std::cout << "=== 示例1:验证default_logger是同一个实例 ===" << std::endl;
    
    // 获取默认logger两次
    auto logger1 = spdlog::default_logger();
    auto logger2 = spdlog::default_logger();
    
    // 输出它们的地址(应该是相同的)
    std::cout << "logger1地址: " << logger1.get() << std::endl;
    std::cout << "logger2地址: " << logger2.get() << std::endl;
    
    // 验证:修改一个会影响另一个
    logger1->set_pattern("[修改后] %v");
    logger1->info("这是logger1的消息");
    logger2->info("这是logger2的消息");  // 也会使用新格式
    
    std::cout << "\n=== 示例2:直接使用spdlog全局函数 ===" << std::endl;
    
    // 这些全局函数使用默认logger
    spdlog::info("直接调用info");
    spdlog::warn("直接调用warn");
    spdlog::error("直接调用error");
    
    // 等价于:
    spdlog::default_logger()->info("等价于上面");
    
    return 0;
}

都是一个意思。都是在操作了自己内置的那个logger。

1.2.4.日志记录器类2------自定义日志记录器类

除了这个默认的logger,由于整个程序只有一个默认logger,所有全局函数调用都使用同一个默认logger实例,万一我们需要更多定制化的操作怎么办?这个时候我们就可以自己来创建自己的logger

输出目标的指定------Sink

一、什么是Sink?

  • Sink是日志的目的地,即日志写到哪里去。
  • 每个Sink代表一种日志输出方式,比如输出到控制台、文件、网络等。

二、Sink的分类(按输出目标)

控制台Sink:将日志输出到控制台(终端)

  1. stdout_sink_mt:输出到标准输出(白色文字)
  2. stderr_sink_mt:输出到标准错误
  3. stdout_color_sink_mt:输出到标准输出,且不同级别用不同颜色
  4. stderr_color_sink_mt:输出到标准错误,且不同级别用不同颜色

文件Sink:将日志输出到文件

  • basic_file_sink_mt:输出到单个文件,文件会不断增大
  • rotating_file_sink_mt:滚动文件,当文件达到指定大小后,会新建一个文件,并保留指定数量的文件

网络Sink:将日志输出到网络

  • tcp_sink_mt:通过TCP协议发送日志到远程服务器
  • udp_sink_mt:通过UDP协议发送日志到远程服务器
  • kafka_sink_mt:将日志发送到Kafka消息队列
  • mongo_sink_mt:将日志存储到MongoDB数据库

其他Sink:如系统日志、事件日志等(根据平台不同)

三、Sink的线程安全版本(_mt)和非线程安全版本(_st)

_mt(多线程版本):

  • 线程安全,可以在多个线程中同时使用,内部有锁机制。
  • 适用于多线程程序,但性能略低于_st版本。

_st(单线程版本):

  • 非线程安全,只能在单个线程中使用,没有锁,因此性能更高。
  • 适用于单线程程序,或者确保只在单线程中使用的场景。

那么我们这里只需要了解两种输出目标(其实就是两个类)

  • 标准输出彩色日志输出目标(多线程版):spdlog::sinks::stdout_color_sink_mt
  • 基础文件日志输出目标(多线程版):spdlog::sinks::basic_file_sink_mt

对于这个基础文件日志输出目标,我想多说几嘴

cpp 复制代码
basic_file_sink_mt("文件路径", 是否清空文件)

文件路径:如 "logs/app.log"

是否清空文件:

  • true = 程序启动时清空文件内容(从头开始写)
  • false = 在文件末尾追加(保留之前的日志)

我们直接看例子

cpp 复制代码
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>  // 控制台带颜色输出
#include <spdlog/sinks/basic_file_sink.h>     // 文件输出

int main() {
    // ========== 示例1:控制台输出 ==========
    std::cout << "=== 示例1:控制台输出 ===" << std::endl;
    
    // 创建控制台输出目标(带颜色)
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    
    // 创建logger并指定控制台输出
    spdlog::logger console_logger("console_logger", console_sink);
    console_logger.set_level(spdlog::level::info);
    
    console_logger.info("这条日志会显示在控制台");
    console_logger.warn("这是警告信息(通常显示为黄色)");
    console_logger.error("这是错误信息(通常显示为红色)");
    
    // ========== 示例2:文件输出 ==========
    std::cout << "\n=== 示例2:文件输出 ===" << std::endl;
    
    // 创建文件输出目标(日志保存到文件)
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log", true);
    
    spdlog::logger file_logger("file_logger", file_sink);
    file_logger.set_level(spdlog::level::debug);
    
    file_logger.debug("调试信息会保存到文件");
    file_logger.info("这条信息也会保存到logs/app.log文件中");
    
    // ========== 示例3:同时输出到控制台和文件 ==========
    std::cout << "\n=== 示例3:同时输出到控制台和文件 ===" << std::endl;
    
    // 创建多个输出目标
    std::vector<spdlog::sink_ptr> sinks;
    sinks.push_back(console_sink);  // 输出到控制台
    sinks.push_back(file_sink);     // 输出到文件
    
    // 创建logger并指定多个输出目标
    spdlog::logger multi_sink_logger("multi_logger", sinks.begin(), sinks.end());
    multi_sink_logger.set_level(spdlog::level::debug);
    
    // 这条日志会同时显示在控制台和写入文件
    multi_sink_logger.info("这条日志会同时输出到控制台和文件");
    multi_sink_logger.error("错误信息也会同时输出到两个地方");
    
    return 0;
}

注意:spdlog::sink_ptr 就是日志输出目标的智能指针

cpp 复制代码
// 这是spdlog内部定义
using sink_ptr = std::shared_ptr<spdlog::sinks::sink>;
  • std::shared_ptr = 智能指针(自动管理内存)
  • spdlog::sinks::sink = 所有输出目标的基类

所以 sink_ptr = 智能指针指向输出目标基类

刷新策略

我们的日志也可能不是立即到达这个输出目标的,他也有他自己的刷新策略,那么我们就可以通过下面这个接口来进行刷新

cpp 复制代码
// 立即刷新所有缓存日志到输出目标
logger.flush();

// 设置当日志级别达到或超过warn时立即刷新
logger.flush_on(spdlog::level::warn);

// 这意味着:
// trace, debug, info 级别的日志可能被缓存
// warn, error, critical 级别的日志会立即输出

设置日志级别

cpp 复制代码
// 只记录info及以上级别的日志
logger.set_level(spdlog::level::info);

// 设置不同的级别
logger.set_level(spdlog::level::trace);  // 记录所有日志
logger.set_level(spdlog::level::error);  // 只记录error和critical

设置输出格式

cpp 复制代码
// 自定义日志格式
logger.set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v");
// %Y-%m-%d: 年-月-日
// %H:%M:%S: 时:分:秒
// %l: 日志级别
// %v: 日志消息

记录日志

这个是logger类最核心的功能。

我们记住下面6个接口,这6个是最重要的

cpp 复制代码
​logger->trace("This is a trace message"); 
logger->debug("This is a debug message"); 
logger->info("This is an info message"); 
logger->warn("This is a warning message"); 
logger->error("This is an error message"); 
logger->critical("This is a critical message");

main1.cpp

我们看看最简单的用法

cpp 复制代码
#include <spdlog/spdlog.h>
#include <iostream>

int main() {
    // 创建一个默认的logger(输出到控制台)
    auto logger = spdlog::default_logger();
    
    std::cout << "=== 示例1:基本日志输出 ===" << std::endl;
    
    // 基本用法:直接输出字符串
    logger->info("应用程序启动");
    logger->warn("请注意:测试模式");
    logger->error("发生了一个错误");
    
    return 0;
}

编译语句都是

cpp 复制代码
g++ main.cpp -o test -lspdlog -lfmt

main2.cpp

我们看看格式化输出版本

cpp 复制代码
#include <string>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>  // 控制台带颜色输出
#include <spdlog/sinks/basic_file_sink.h>     // 文件输出

int main() {
    auto logger = spdlog::default_logger();
    
    // 创建控制台输出目标(带颜色)
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    // 创建logger并指定控制台输出
    spdlog::logger console_logger("console_logger", console_sink);
    
    // 示例变量
    std::string username = "张三";
    int userId = 1001;
    double accountBalance = 1234.56;
    bool isVip = true;
    const char* ip = "192.168.1.100";
    
    // 格式化输出,使用 {} 作为占位符
    logger->info("用户 {} 登录成功", username);                    // 字符串参数
    logger->info("用户ID: {}, IP地址: {}", userId, ip);           // 整数和字符串
    logger->info("余额: {} 元", accountBalance);                  // 浮点数
    logger->info("VIP用户: {}", isVip);                          // 布尔值
    logger->info("用户信息: ID={}, 姓名={}, 余额={}", 
                 userId, username, accountBalance);              // 多个参数
    
    return 0;
}

编译语句都是

cpp 复制代码
g++ main.cpp -o test -lspdlog -lfmt

main3.cpp

cpp 复制代码
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>  // 控制台带颜色输出
#include <spdlog/sinks/basic_file_sink.h>     // 文件输出
#include <cmath>

int main() {
    auto logger = spdlog::default_logger();
    
    // 创建控制台输出目标(带颜色)
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    // 创建logger并指定控制台输出
    spdlog::logger console_logger("console_logger", console_sink);
    
    double pi = 3.141592653589793;
    int number = 255;
    double largeNum = 1234567.89;
    
    // 控制浮点数精度
    logger->info("圆周率: {:.2f}", pi);            // 保留2位小数
    logger->info("圆周率: {:.4f}", pi);            // 保留4位小数
    logger->info("圆周率: {:8.3f}", pi);           // 总宽度8,保留3位小数
    
    // 整数格式化
    logger->info("十进制: {}", number);            // 十进制
    logger->info("十六进制: {:x}", number);        // 十六进制
    logger->info("十六进制(大写): {:X}", number);   // 十六进制大写
    logger->info("八进制: {:o}", number);          // 八进制
    logger->info("二进制: {:b}", number);          // 二进制
    
    // 对齐和填充
    logger->info("左对齐: {:<10} 结束", "ABC");    // 左对齐,宽度10
    logger->info("右对齐: {:>10} 结束", "ABC");    // 右对齐,宽度10
    logger->info("居中对齐: {:^10} 结束", "ABC");  // 居中对齐,宽度10
    
    // 数字格式
    logger->info("科学计数法: {:e}", largeNum);    // 1.234568e+06
    
    return 0;
}

1.2.5.spdlog日志记录器工厂类

工厂类就是"日志记录器生成器",帮你用一行代码创建配置好的logger,不用自己拼装各种组件。

对比:不用工厂 vs 用工厂

cpp 复制代码
// ❌ 不用工厂(麻烦)
#include <spdlog/sinks/stdout_color_sinks.h>
#include <memory>

// 要写很多代码
auto sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();//要指定sink
auto logger = std::make_shared<spdlog::logger>("my_logger", sink);//要自己创建logger对象
logger->set_pattern(...);
logger->set_level(...);

// ✅ 用工厂(简单)
#include <spdlog/spdlog.h>

// 一行代码搞定!
auto logger = spdlog::stdout_color_mt("my_logger");

这些工厂函数会创建一个logger,并且已经附加了对应的sink(日志输出地址)。所以,我们不需要自己创建sink然后组装到logger上,直接使用这些工厂函数即可。

那么,都有哪些工厂类呢?

一、控制台输出工厂函数

  1. 彩色控制台输出
  • stdout_color_mt - 彩色标准输出(多线程安全,同步)

  • stdout_color_st - 彩色标准输出(单线程,同步)

  • stderr_color_mt - 彩色标准错误输出(多线程安全,同步)

  • stderr_color_st - 彩色标准错误输出(单线程,同步)

  1. 无颜色控制台输出
  • stdout_logger_mt - 标准输出无颜色(多线程安全,同步)

  • stdout_logger_st - 标准输出无颜色(单线程,同步)

  • stderr_logger_mt - 标准错误输出无颜色(多线程安全,同步)

  • stderr_logger_st - 标准错误输出无颜色(单线程,同步)

二、文件输出工厂函数

  1. 基础文件输出
  • basic_logger_mt - 基础文件输出(多线程安全,同步)

  • basic_logger_st - 基础文件输出(单线程,同步)

  1. 循环文件输出(按大小)
  • rotating_logger_mt - 按大小轮转文件(多线程安全,同步)

  • rotating_logger_st - 按大小轮转文件(单线程,同步)

  1. 每日文件输出
  • daily_logger_mt - 每日轮转文件(多线程安全,同步)

  • daily_logger_st - 每日轮转文件(单线程,同步)

  1. 每小时文件输出
  • hourly_logger_mt - 每小时轮转文件(多线程安全,同步)

  • hourly_logger_st - 每小时轮转文件(单线程,同步)

......

上面这些都是工厂类,这些工厂函数会创建一个logger,并且已经附加了对应的sink。所以,我们不需要自己创建sink然后组装到logger上,直接使用这些工厂函数即可。

我们就来看看这个到底怎么使用?我们以这个stdout_color_mt为例

  • stdout_color_mt - 彩色标准输出(多线程安全,同步)
cpp 复制代码
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
int main() {
    // 创建彩色控制台日志记录器(多线程安全)
    auto console_logger = spdlog::stdout_color_mt("my_console_logger");
    //每个 logger 都有一个唯一的名字,用来区分不同的 logger
    //我们这里的logger唯一的名字是my_console_logger
    
    // 使用日志记录器
    console_logger->info("这是一条信息日志");
    console_logger->warn("这是一条警告日志(黄色)");
    console_logger->error("这是一条错误日志(红色)");
    
    return 0;
}

至于其他的,你们自己去试着去用即可。

但是,大家需要注意一个情况:spdlog并没有一个直接的工厂函数来创建一个多sink的日志器。

也就是说,我们使用工厂类,不能直接生成一个日志可以同时输出到多个目的地(控制台和文件)的日志控制器。这个时候,我们就还是必须使用spdlog::logger类来实现

异步版本

我们先看看同步和异步日志有啥区别???

  • 同步日志:logger->info("xxx") 会等待写入完成才继续执行
  • 异步日志:logger->info("xxx") 立即返回,后台线程帮你写入

我们上面那些所有的工厂类,都是默认是创建一个同步版本的日志记录器

cpp 复制代码
// 默认是同步的
auto logger = spdlog::stdout_color_mt("my_logger");

那么如果我们需要创建异步版本的日志记录器,那么我们就需要加上异步工厂模板参数,就像下面这样子

cpp 复制代码
// 加上 <spdlog::async_factory> 变成异步
auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("my_logger");

那么异步工厂模板参数一共有2种

  1. async_factory - 异步工厂(阻塞模式)

  2. async_factory_nb - 异步工厂(非阻塞模式)

任何工厂函数都可以加上 <spdlog::async_factory> 或 <spdlog::async_factory_nb> 变成异步版本,大家按需求去设定即可

注意:异步工厂的关键点:

  • 必须先用 spdlog::init_thread_pool() 初始化线程池
  • 所有工厂函数都可以加上 <spdlog::async_factory> 变成异步(当然这个async_factory_nb也是可以的)
  • 程序结束前必须调用 spdlog::shutdown()
  • 根据场景选择阻塞或非阻塞策略
  • 异步日志提升性能,但可能丢失队列中的日志

记住这个万能公式:

cpp 复制代码
spdlog::init_thread_pool(队列大小, 线程数);  // 第一步
auto logger = 任何工厂函数<spdlog::async_factory>(参数);  // 第二步
// 使用logger...
spdlog::shutdown();  // 最后一步

(当然这个async_factory_nb也是可以的)

那么话不多说,我们直接使用一下

示例

同步版本

cpp 复制代码
#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h"

int main() {
    // 1. 创建同步日志器(前台写入)
    auto logger = spdlog::basic_logger_mt("sync_logger", "logs/my_log.txt");
    
    // 2. 使用它
    logger->info("这是一条日志");
    logger->warn("这是一个警告");
    
    return 0;
}

异步版本

cpp 复制代码
#include <iostream>
#include "spdlog/spdlog.h"
#include "spdlog/async.h"      // 异步需要这个头文件
#include "spdlog/sinks/basic_file_sink.h"

int main() {
    // 1. 设置异步日志(后台写入)
    spdlog::init_thread_pool(8192, 1);  // 创建后台线程
    
    // 2. 创建异步日志器(写到文件)
    auto logger = spdlog::basic_logger_mt<spdlog::async_factory>(
        "async_logger", 
        "logs/my_log.txt"
    );
    
    // 3. 使用它!
    logger->info("这是一条日志");
    logger->warn("这是一个警告");
    logger->error("这是一个错误");
    
    std::cout << "日志在后台写入,主线程继续运行..." << std::endl;
    
    // 4. 程序结束前清理
    spdlog::shutdown();
    return 0;
}

我们来看看这两个函数的作用:

  1. spdlog::init_thread_pool(8192, 1);

    • 这是初始化异步日志的线程池。

    • 第一个参数 8192 是队列的最大项目数,也就是日志消息队列最多可以存放8192条日志消息。

    • 第二个参数 1 是线程池中的线程数量,这里设置为1,表示有一个后台线程专门负责写日志。

    • 为什么需要这个?因为异步日志的原理是:当你调用日志记录函数(如info)时,日志消息被放入一个队列,然后由后台线程从队列中取出并写入文件(或其他sink)。这样就不会阻塞主线程。所以,这个线程池就是用来管理这些后台线程的。

  2. spdlog::shutdown();

    • 这是用来关闭所有日志器并释放资源。

    • 在程序退出前,我们需要确保所有的日志都被写入(因为异步日志可能还在队列中,没有来得及写入文件)。调用shutdown会等待所有日志写入完成,然后清理资源。

    • 如果不调用shutdown,程序退出时可能还有日志在队列中没有被写入,就会丢失日志。同时,也可能导致资源(如线程)没有正确释放。

所以,这两个函数是异步日志中非常重要的初始化和清理步骤。

可以说它们完成的任务是一样的

1.2.6.全局接口

我们主要介绍三个全局接口:set_level, flush_every, flush_on。这些接口可以设置日志的级别和刷新策略。

注意:这些接口是全局的,会影响所有日志器(除非你单独设置某个日志器的策略)。

  1. set_level: 设置日志级别,只有不低于该级别的日志才会被输出。

  2. flush_every: 每隔一定时间自动刷新日志(将缓冲区的内容写入到目标,如文件)。

  3. flush_on: 当遇到不低于指定级别的日志时,立即刷新。

一次设置,全局生效 - 所有日志器都会受到影响!

cpp 复制代码
#include <spdlog/spdlog.h>

int main() {
    // 设置全局日志级别为 "警告" (warn)
    spdlog::set_level(spdlog::level::warn);
    
    // 创建默认日志器
    auto logger = spdlog::default_logger();
    
    // 测试不同级别的日志
    logger->debug("调试信息");    // 被过滤掉(看不到)
    logger->info("普通信息");     // 被过滤掉(看不到)
    logger->warn("警告信息");     // 能看到(黄色)
    logger->error("错误信息");     // 能看到(红色)
    
    // 调整级别为 "信息" (info)
    spdlog::set_level(spdlog::level::info);
    logger->info("现在能看到我了!");  // 这次能看到了
}
cpp 复制代码
#include "spdlog/spdlog.h"
#include <thread>

int main() {
    auto logger = spdlog::basic_logger_mt("file_logger", "game_log.txt");
    
    // 每3秒自动刷新一次(把缓冲区的内容写入文件)
    spdlog::flush_every(std::chrono::seconds(3));
    
    // 模拟游戏运行
    for (int i = 1; i <= 10; i++) {
        logger->info("游戏第{}秒", i);
        std::this_thread::sleep_for(std::chrono::seconds(1));
        
        // 假设在第5秒游戏崩溃了...
        if (i == 5) {
            std::cout << "游戏崩溃!但日志已经保存到第3秒了" << std::endl;
            // 没有flush_every的话,可能什么都没保存!
            break;
        }
    }
}
cpp 复制代码
#include "spdlog/spdlog.h"

int main() {
    auto logger = spdlog::basic_logger_mt("system_logger", "system.log");
    
    // 当遇到错误(error)及以上级别的日志时,立即刷新
    spdlog::flush_on(spdlog::level::err);
    
    // 普通日志(不立即刷新,缓冲区积累)
    logger->info("系统启动");
    logger->info("加载配置文件");
    logger->info("初始化数据库");
    
    // 这里假设程序要崩溃了...
    logger->error("数据库连接失败!");  // 立即刷新,确保这条写到文件
    
    // 如果程序在这里崩溃,上面的error日志已经保存了!
    // 而前面的info日志可能还在缓冲区,可能丢失
    
    std::cout << "即使程序马上崩溃,错误日志也已经保存了!" << std::endl;
}

这些例子大家看看就行了,我不会多说一句

1.2.7.多Sink日志示例

但是,大家需要注意一个情况:spdlog并没有一个直接的工厂函数来创建一个多sink的日志器。

也就是说,我们使用工厂类,不能直接生成一个日志可以同时输出到多个目的地(控制台和文件)的日志控制器。这个时候,我们就还是必须使用spdlog::logger类来实现

那么我们现在就来写这么一个例子

这个程序展示了如何创建一个同时输出到控制台和文件的日志器,并且可以为不同的输出目的地设置不同的日志级别。

程序目的:

演示如何将日志同时输出到多个目的地(控制台和文件),并且分别控制每个目的地的日志级别和格式。

程序步骤:

  1. 创建控制台Sink:创建一个彩色的控制台输出Sink,并设置它只接收警告(warn)及以上级别的日志。同时设置了控制台输出的格式,使其显示日志级别并带有颜色。

  2. 创建文件Sink :创建一个文件输出Sink,将日志写入到文件logs/multisink.txt中,并设置它接收所有级别的日志(从trace开始,即最详细的日志)。

  3. 创建日志器:创建一个名为"multi_sink"的日志器,并将上面两个Sink绑定到这个日志器上。然后设置日志器本身的日志级别为debug(这意味着debug及以上级别的日志都会进入日志器,但每个Sink还会根据自己的级别进行过滤)。

  4. 测试日志输出:分别输出debug、info、warn、error级别的日志,以及一条格式化日志。根据设置,控制台只会显示warn和error级别的日志,而文件会记录所有级别的日志。

  5. 清理资源 :在程序结束前,调用spdlog::shutdown()来确保所有日志被刷新并释放资源。

cpp 复制代码
// 多Sink日志示例程序
// 编译命令:g++ -std=c++17 main.cpp -o multi_sink_demo -lspdlog -lfmt
#include <iostream>
#include <spdlog/spdlog.h> 
#include <spdlog/sinks/stdout_color_sinks.h> 
#include <spdlog/sinks/basic_file_sink.h> 

int main() 
{ 
    // 示例1:创建多Sink日志器
    // 一个日志可以同时输出到多个目的地(控制台和文件)
    
    // 1. 创建控制台输出的Sink对象(彩色输出,多线程安全版本)
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); 
    
    // 设置控制台Sink的日志级别:只显示警告(warn)及以上级别的日志
    // 这样info和debug级别的日志就不会显示在控制台
    console_sink->set_level(spdlog::level::warn); 
    
    // 设置控制台输出的日志格式
    // [multi_sink_example] [级别] 日志内容
    // %^%l%$:%l表示日志级别,%^和%$表示颜色开始和结束标记
    console_sink->set_pattern("[multi_sink_example] [%^%l%$] %v"); 
    
    // 2. 创建文件输出的Sink对象
    // 参数1:日志文件名,参数2:true表示清空原有文件内容
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/multisink.txt", true); 
    
    // 设置文件Sink的日志级别:显示所有级别的日志(从trace开始)
    // trace是最详细的日志级别,会记录所有信息
    file_sink->set_level(spdlog::level::trace); 
    
    // 3. 创建日志器对象,同时绑定两个Sink
    // 参数1:日志器名称,参数2:Sink列表
    spdlog::logger logger("multi_sink", {console_sink, file_sink}); 
    
    // 设置日志器的全局日志级别:debug及以上级别都会经过日志器
    // 注意:日志器级别是初步过滤,然后每个Sink还会根据自己设置的级别进行二次过滤
    logger.set_level(spdlog::level::debug); 
    
    // 设置日志器的默认日志格式
    // 年-月-日 时:分:秒 [级别] 日志内容
    logger.set_pattern("%Y-%m-%d %H:%M:%S [%l] %v"); 
    
    // 4. 测试不同级别的日志输出
    
    std::cout << "=== 开始测试多Sink日志 ===" << std::endl;
    std::cout << "控制台:只显示warn及以上级别的日志" << std::endl;
    std::cout << "文件:显示所有级别的日志" << std::endl;
    std::cout << std::endl;
    
    // 输出debug级别的日志:不会显示在控制台,但会写入文件
    logger.debug("这是一条debug级别的日志 - 只写入文件,不在控制台显示");
    
    // 输出info级别的日志:不会显示在控制台,但会写入文件
    logger.info("这是一条info级别的日志 - 只写入文件,不在控制台显示");
    
    // 输出warn级别的日志:会在控制台和文件中都显示
    logger.warn("这是一条warn级别的日志 - 控制台和文件都会显示");
    
    // 输出error级别的日志:会在控制台和文件中都显示
    logger.error("这是一条error级别的日志 - 控制台和文件都会显示");
    
    // 输出格式化日志
    int count = 100;
    double price = 99.99;
    std::string product = "测试产品";
    logger.info("产品销售统计:产品={},数量={},单价={:.2f}", product, count, price);
    
    std::cout << std::endl;
    std::cout << "=== 测试完成 ===" << std::endl;
    std::cout << "请查看 logs/multisink.txt 文件查看完整日志" << std::endl;
    
    // 5. 清理资源
    spdlog::shutdown();
    
    return 0; 
}

1.2.8.异步日志示例

对于异步日志,我们更应该好好的去了解一下

cpp 复制代码
// 异步日志示例程序
// 编译命令:g++ -std=c++17 main_async.cpp -o async_demo -lspdlog -lfmt

#include <spdlog/spdlog.h> 
#include <spdlog/sinks/basic_file_sink.h> 
#include <spdlog/async.h> 
#include <iostream>
#include <chrono>
#include <thread>

int main() 
{ 
    // 示例2:创建异步日志器
    // 异步日志不会阻塞主线程,适合高性能场景
    
    std::cout << "=== 开始测试异步日志 ===" << std::endl;
    std::cout << "异步日志会在后台线程写入文件,不会阻塞主线程" << std::endl;
    std::cout << std::endl;
    
    // 1. 初始化异步日志的线程池
    // 参数1:队列大小(32768) - 最多可以缓存32768条日志
    // 参数2:线程数量(1) - 使用1个后台线程处理日志
    std::cout << "初始化线程池..." << std::endl;
    spdlog::init_thread_pool(32768, 1);
    
    // 2. 通过工厂模式创建异步日志记录器
    // 注意:basic_logger_mt后面的<spdlog::async_factory>表示使用异步工厂
    // 参数1:日志器名称
    // 参数2:日志文件名
    std::cout << "创建异步日志器..." << std::endl;
    auto async_logger = spdlog::basic_logger_mt<spdlog::async_factory>(
        "async_file_logger", 
        "logs/async_log.txt"
    ); 
    
    // 3. 设置异步日志器的日志格式
    // 年-月-日 时:分:秒 [级别] 日志内容
    async_logger->set_pattern("%Y-%m-%d %H:%M:%S [%l] %v"); 
    
    // 4. 设置全局日志级别为info
    // 这样debug级别的日志会被过滤掉
    async_logger->set_level(spdlog::level::info);
    
    // 5. 测试异步日志性能
    
    std::cout << "开始写入1000条异步日志..." << std::endl;
    std::cout << "主线程不会阻塞,可以继续执行其他任务" << std::endl;
    
    // 记录开始时间
    auto start_time = std::chrono::high_resolution_clock::now();
    
    // 循环输出1000条异步日志
    for (int i = 1; i <= 1000; ++i) { 
        // 注意:spdlog使用{}作为占位符,可以自动识别数据类型
        async_logger->info("异步日志消息 #{} - 当前时间: {}", i, time(nullptr)); 
        
        // 每100条日志输出一次进度
        if (i % 100 == 0) {
            std::cout << "已提交 " << i << " 条日志到异步队列" << std::endl;
        }
        
        // 模拟主线程的其他工作
        // 如果使用同步日志,这里会被阻塞
        // 但使用异步日志,主线程可以继续执行
        std::this_thread::sleep_for(std::chrono::microseconds(100));
    }
    
    // 记录结束时间
    auto end_time = std::chrono::high_resolution_clock::now();
    
    // 计算耗时
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
    
    std::cout << std::endl;
    std::cout << "=== 异步日志性能测试结果 ===" << std::endl;
    std::cout << "总耗时: " << duration.count() << " 毫秒" << std::endl;
    std::cout << "平均每条日志耗时: " << duration.count() / 1000.0 << " 毫秒" << std::endl;
    std::cout << std::endl;
    
    // 6. 输出一些其他级别的日志
    async_logger->warn("这是一个异步警告消息");
    async_logger->error("这是一个异步错误消息");
    
    // 7. 演示格式化功能
    std::cout << "=== 测试异步日志格式化功能 ===" << std::endl;
    
    int apples = 10;
    double price_per_apple = 2.5;
    std::string fruit = "苹果";
    
    // 自动识别数据类型并格式化
    async_logger->info("水果销售:{}个{},单价{:.2f}元,总价{:.2f}元", 
                      apples, fruit, price_per_apple, apples * price_per_apple);
    
    // 8. 清理资源
    std::cout << std::endl;
    std::cout << "等待所有异步日志写入完成..." << std::endl;
    spdlog::shutdown();  // 这会等待所有异步日志写入完成
    
    std::cout << "=== 异步日志测试完成 ===" << std::endl;
    std::cout << "请查看 logs/async_log.txt 文件查看日志内容" << std::endl;
    
    return 0; 
}

程序执行流程:

  1. 程序开始,打印开始测试的提示信息。

  2. 初始化线程池:创建一个可以存储32768条日志的队列,并启动1个后台线程来处理队列中的日志(即将日志写入文件)。

  3. 创建异步日志记录器:该日志器将日志写入文件"logs/async_log.txt",并且使用异步方式(即日志消息先放入队列,然后由后台线程写入文件)。

  4. 设置日志格式和日志级别:格式为"年-月-日 时:分:秒 [级别] 日志内容",级别设置为info,这样debug级别的日志会被过滤掉。

  5. 测试异步日志性能:记录开始时间,然后循环1000次,每次写入一条异步日志(包含序号和当前时间),同时每100条打印一次进度。在每次写入日志后,主线程睡眠100微秒,模拟其他工作。记录结束时间,并计算总耗时和平均每条日志的耗时(注意:这里计算的是主线程提交日志的时间,不包括后台线程写入文件的时间,因为异步日志的提交是非阻塞的)。

  6. 输出其他级别的日志(警告和错误)以及一条格式化的日志(水果销售信息)。

  7. 清理资源:调用shutdown,等待后台线程将队列中所有日志写入文件,然后程序结束。

注意:由于是异步日志,主线程提交日志的速度可能快于后台线程写入的速度,但因为有队列,所以主线程不会被阻塞。但是,如果队列满了,主线程再提交日志就会被阻塞(除非设置其他策略)。在这个例子中,队列大小为32768,足够大,所以不会阻塞。

运行这个程序,你会看到控制台输出提交日志的进度,并且程序运行的总时间会非常短(因为提交日志只是将日志放入队列,然后立即返回)。但是,请注意,实际上后台线程可能还在处理队列中的日志,所以在最后调用shutdown来确保所有日志都被写入文件。

1.3.封装spdlog进行二次封装

封装原因

  1. 避免单例模式的锁竞争

    spdlog 默认提供的单例日志器在多线程高并发场景下可能出现锁竞争,影响性能。因此,我们选择直接创建全局的、线程安全的日志器对象,每个线程独立使用,从而减少锁冲突。

  2. 增强日志信息可追溯性

    spdlog 默认输出不包含文件名和行号,在调试时难以快速定位日志来源。因此,我们通过宏封装,在输出日志时自动附加**FILELINE**信息,提升调试效率。

  3. 提供统一的初始化接口

    为简化日志器的配置与使用,我们封装一个统一的初始化接口,根据运行模式动态设置日志输出目标与级别,使日志配置更加灵活和集中。

封装核心思想

设计一个全局可访问的日志接口,用户通过该接口完成日志器的创建与初始化,无需关心内部实现细节。

初始化接口设计

初始化函数支持以下参数,以适应不同运行环境:

  1. 运行模式bool 类型)

    • true 表示调试模式:日志输出至标准输出(控制台),便于实时查看。

    • false 表示发布模式:日志输出至指定文件,用于持久化存储。

  2. 输出文件名std::string 类型)

    • 在发布模式下,指定日志文件的存储路径与名称。
  3. 输出日志等级spdlog::level::level_enum 类型)

    • 设置日志输出级别,在发布模式下可灵活控制日志详细程度,提升运行效率。

那么我们其实也很快就能封装出这个日志函数,大家直接看源码即可。

cpp 复制代码
#pragma once
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/async.h>
#include <iostream>

// mode - 运行模式: true-发布模式; false调试模式

namespace instant_messenger
{
    // 全局日志器实例,使用shared_ptr管理生命周期
    // 该日志器默认初始化为空指针,需要调用init_logger进行初始化
    std::shared_ptr<spdlog::logger> g_default_logger;

    // 初始化全局日志器
    // mode: 运行模式,false为调试模式,true为发布模式
    // file: 发布模式下日志输出文件的路径
    // level: 发布模式下日志输出等级,使用spdlog::level::level_enum枚举值

    // 注意这个level有下面这几种:
    // trace (0) - 最详细的日志级别,用于追踪程序的执行流程
    // debug (1) - 调试信息,用于开发过程中排查问题
    // info (2) - 一般信息,用于记录程序正常运行状态
    // warn (3) - 警告信息,表示可能出现问题但程序仍可继续运行
    // err (4) - 错误信息,表示发生了错误但程序仍可尝试恢复
    // critical (5) - 严重错误,表示程序无法继续正常运行
    // off (6) - 关闭所有日志输出

    void init_logger(bool mode, const std::string &file, int32_t level)
    {
        if (mode == false) // 调试模式
        {
            // 调试模式:创建标准输出日志器(控制台输出)
            // 使用彩色输出,便于在控制台中区分不同级别的日志
            g_default_logger = spdlog::stdout_color_mt("default-logger"); // 利用了工厂类来创建logger对象,采用同步模式
            // 工厂类stdout_color_mt - 彩色标准输出(多线程安全,同步)

            // 设置日志输出等级为trace(最低级别),输出所有日志
            g_default_logger->set_level(spdlog::level::level_enum::trace);

            // 设置立即刷新等级为trace,确保所有日志立即输出到控制台
            g_default_logger->flush_on(spdlog::level::level_enum::trace);
            // flush_on: 当遇到不低于指定级别的日志时,立即刷新
        }
        else // 发布模式
        {
            // 发布模式:创建文件输出日志器
            // 将日志输出到指定文件中,便于后续分析和问题排查
            g_default_logger = spdlog::basic_logger_mt("default-logger", file);

            // 设置日志输出等级,根据传入参数确定输出级别
            // 使用强制类型转换将int32_t转换为spdlog的日志等级枚举
            g_default_logger->set_level((spdlog::level::level_enum)level);

            // 设置立即刷新等级,与输出等级保持一致
            g_default_logger->flush_on((spdlog::level::level_enum)level);
            // flush_on: 当遇到不低于指定级别的日志时,立即刷新
        }

        // 设置统一的日志输出格式
        // 格式说明:
        // [%n] - 日志器名称,就比如上面的default-logger就是日志器名称
        // [%H:%M:%S] - 时间戳(时:分:秒)
        // [%t] - 线程ID
        // [%-8l] - 日志级别(左对齐,宽度为8)
        // %v - 实际的日志消息
        g_default_logger->set_pattern("[%n][%H:%M:%S][%t][%-8l]%v");
    }
//注意,我们之前是使用%s来进行占位的,但是spdlog是使用{}来进行占位的,那么这个__FILE__和__LINE__会替换掉对应的{}
#define LOG_TRACE(format, ...) instant_messenger::g_default_logger->trace(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_DEBUG(format, ...) instant_messenger::g_default_logger->debug(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_INFO(format, ...) instant_messenger::g_default_logger->info(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_WARN(format, ...) instant_messenger::g_default_logger->warn(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) instant_messenger::g_default_logger->error(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_FATAL(format, ...) instant_messenger::g_default_logger->critical(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)

//这6个宏其实就是封装了logger类的下面6个日志打印函数
/*
​logger->trace("This is a trace message"); 
logger->debug("This is a debug message"); 
logger->info("This is an info message"); 
logger->warn("This is a warning message"); 
logger->error("This is an error message"); 
logger->critical("This is a critical message");
*/
}

__FILE__和__LINE__的含义

您可以把__FILE__和__LINE__理解为两个"魔法标记",它们的作用是在编译时,由编译器自动填入当前代码所在的位置信息。

它们的具体含义如下:

  1. FILE
  • 它是什么:它是一个字符串常量。
  • 它代表什么:它代表了**当前源代码文件的完整路径名或文件名。**编译器在处理这行代码时,会将它替换成当前源文件的名字。例如,如果你的文件叫做 main.c,那么 FILE 就会被替换为 "main.c"。在某些编译环境中,它可能会包含完整的文件路径,如 "D:/project/src/main.c"。
  1. LINE
  • 它是什么:它是一个整数常量。
  • 它代表什么:它代表了**当前代码在源文件中的行号。**编译器会将它替换成一个数字,这个数字就是这行代码(即 LINE 这行本身)在文件中的具体行数。如果你把这段代码移动到文件的第50行,那么 LINE 的值在下次编译时就会变成50。
cpp 复制代码
#include<iostream>
//封装一个日志宏,通过日志宏进行日志的打印,将打印的信息前带有系统事件,文件名,行号
// 例如:   [17.22.58 log.cpp:12]打开文件失败
int main()
{
    printf("[%s:%d] Hello World!\n",__FILE__,__LINE__);
}

可以看到。

##__VA_ARGS__的由来

C99标准引入了不定参数宏,允许宏接受可变数量的参数。语法类似于可变参数函数,使用 ... 表示可变参数部分,并在替换部分使用 VA_ARGS 来引用这些参数。

cpp 复制代码
#define PRINT(...) printf(__VA_ARGS__)

int main() {
    PRINT("Hello, %s!\n", "world");
    PRINT("Number: %d\n", 42);
    return 0;
}

注意:在C语言中,字符串常量相邻会自动连接成一个字符串

因为format是一个字符串参数,在预处理时,它会被替换成用户传入的字符串,然后与周围的字符串连接,形成一个完整的格式字符串。

cpp 复制代码
#define LOG(format, ...) printf("[%s:%d] " format "\n", __FILE__, __LINE__, __VA_ARGS__)

// 使用示例
int x = 42;
LOG("Value: %d", x);
cpp 复制代码
// 宏展开后的代码:
printf("[%s:%d] " "Value: %d" "\n", __FILE__, __LINE__, x);
// 编译器会处理为:
printf("[%s:%d] Value: %d\n", __FILE__, __LINE__, x);

但是现在还有一个问题。

如果说我传递的是只是1个参数进去

cpp 复制代码
LOG(LDBG);

那么宏函数的不定参数就会报错啊。

上面的LOG宏定义中,format 和 ... 是分开的,这样调用时就需要至少两个参数(level和format),然后可变参数至少一个(因为__VA_ARGS__至少需要一个参数)。

  • 使用C99标准中的__VA_ARGS__,并确保在调用时至少提供一个参数(但这样就不能完全省略可变参数)。
  • 使用##VA_ARGS(GCC扩展),这样当可变参数为空时,就没有一点问题。

如果你希望允许可变参数为空,则需要使用**##VA_ARGS**。

测试

那么现在我们就写一个小程序进行测试一下

cpp 复制代码
#include "logger.hpp"
#include <gflags/gflags.h>

DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

int main(int argc, char *argv[])
{
    google::ParseCommandLineFlags(&argc, &argv, true);
    //通过命令行参数来设定这个logger
    init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
    LOG_DEBUG("你好:{}", "世界");
    LOG_INFO("你好:{}", "世界");
    LOG_WARN("你好:{}", "世界");
    LOG_ERROR("你好:{}", "世界");
    LOG_FATAL("你好:{}", "世界");
    LOG_DEBUG("这是一个测试");
    return -1;
}

没有一点问题。

cpp 复制代码
./main --run_mode=true --log_file=./main.log --log_level=3

执行这个可以看到它就不在这个控制台进行输出了,它把日志打印到main.log里面去了

相关推荐
杜子不疼.2 小时前
内网监控工具翻身!Uptime Kuma+cpolar 实现远程运维自由
linux·运维·服务器
范纹杉想快点毕业2 小时前
STM32单片机与ZYNQ PS端 中断+状态机+FIFO 综合应用实战文档(初学者版)
linux·数据结构·数据库·算法·mongodb
拾光Ծ2 小时前
【Linux】Ext系列文件系统(一):初识文件系统
linux·运维·服务器·硬件架构·ext文件系统
陌上花开缓缓归以2 小时前
insmod 报错问题定位纪要
linux·arm开发
天荒地老笑话么2 小时前
Vim核心快捷键与运维实战指南
运维·vim·excel
一叶龙洲2 小时前
点击ubuntu dock 运行中的应用切换最小化
ubuntu
一叶龙洲2 小时前
ubuntu 25.10安装oh-my-zsh
linux·ubuntu
IT19952 小时前
Linux笔记-使用systemd管理进程
linux·运维·笔记
Web极客码2 小时前
WordPress 在哪里存储网站上的图片?
运维·服务器·wordpress