一. spdlog 介绍

spdlog 是一个高性能、超快速、零配置的 C++ 日志库,它旨在提供简洁的 API 和丰富的功能,同时保持高性能的日志记录。它支持多种输出目标、格式化选项、线程安全以及异步日志记录。以下是对 spdlog 的详细介绍和使用方法。

github 链接:https://github.com/gabime/spdlog

二. spdlog 特点

  • 高性能:spdlog 专为速度而设计,即使在高负载情况下也能保持良好的性能。
  • 零配置:无需复杂的配置,只需包含头文件即可在项目中使用。
  • 异步日志:支持异步日志记录,减少对主线程的影响。
  • 格式化:支持⾃定义日志消息的格式化,包括时间戳、线程 ID、日志级别等。
  • 多平台:跨平台兼容,支持 Windows、Linux、macOS 等操作系统。
  • 丰富的 API:提供丰富的日志级别和操作符重载,方便记录各种类型的日志。

glog 和 spdlog 都是流行的 C++ 日志库,它们各自具有不同的特点和优势。以下是对这两个库的对比分析,包括性能测试的结果和使用场景的考量。

glog 是由 Google 开发的一个开源 C++ 日志库,它提供了丰富的日志功能,包括多种日志级别、条件日志记录、日志文件管理、信号处理、自定义日志格式等。glog 默认情况下是同步记录日志的,这意味着每次写日志操作都会阻塞直到日志数据被写入磁盘。

根据性能对比测试分析,glog 在同步调用的场景下的性能较 spdlog 慢。在一台低配的服务器上,glog 耗时1.027秒处理十万笔日志数据,而在固态硬盘上的耗时为0.475秒。

spdlog 是一个开源的、高性能的 C++ 日志库,它支持异步日志记录,允许在不影响主线程的情况下进行日志写入。spdlog 旨在提供零配置的用户体验,只需包含头文件即可使用。它还支持多种输出目标、格式化选项和线程安全。

在同样的性能测试中,spdlog 在同步调⽤的场景下比 glog 快。在低配服务器上的耗时为0.135秒,而在固态硬盘上的耗时为0.057秒。此外 spdlog 还提供了异步日志记录的功能,其简单异步模式的耗时为0.158秒。

对比总结:

  • 性能:从性能测试结果来看,spdlog 在同步调用场景下的性能优于 glog。当涉及到大量日志数据时,spdlog 显示出更快的处理速度。
  • 异步日志:spdlog 支持异步日志记录,这在处理高负载应用程序时非常有用,可以减少日志操作对主线程的影响。
  • 易用性:spdlog 提供了更简单的集成和配置方式,只需包含头文件即可使用,而 glog 可能需要额外的编译和配置步骤。
  • 功能:glog 提供了一些特定的功能,如条件日志记录和信号处理,在某些场景下可能非常有用。
  • 使用场景:glog 可能更适合那些对日志性能要求不是特别高,但需要一些特定功能的场景。而 spdlog 则适合需要高性能日志记录和异步日志能力的应用程序。

在选择日志库时,开发者应根据项,项目的具体需求和性能要求来决定使用哪个库。如果项目对日志性能有较高要求,或者需要异步日志记录来避免阻塞主线程,spdlog 可能是更好的选择。如果项目需要一些特定的日志功能,或者已经在使用 glog 且没有显著的性能问题,那么继续使用 glog 也是合理的。

总结:spdlog 是一个功能强大且易于使用的 C++ 日志库,它提供了丰富的功能和高性能的日志记录能力。通过简单的 API,开发者可以快速地在项目中实现日志记录,同时保持代码的清晰和可维护性。无论是在开发阶段还是生产环境中,spdlog 都能提供稳定和高效的日志服务。

三. spdlog 安装

命令安装:

sudo apt-get install libspdlog-dev

原码安装:

# 下载源码
git clone https://github.com/gabime/spdlog.git
# 切换目录
cd spdlog/
mkdir build
cd build
# 生成 Makefile
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
# 编译代码
make
# 安装
sudo make install

四. spdlog 使用

1. 头文件和链接库

在这里插入图片描述

#include <spdlog/spdlog.h>

程序编译时库的链接

-lspdlog -lfmt

2. 日志输出等级枚举

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
    };
}

3. 日志输出格式自定义

可以自定义日志消息的格式:

logger->set_pattern("%Y-%m-%d %H:%M:%S [%t] [%^%l%$] %v");

%t - 线程ID(Thread ID)
%n - 日志器名称(Logger name)
%l - 日志级别名称(Level name),如 INFO, DEBUG, ERROR 等
%v - 日志内容(message)
%Y - 年(Year)
%m - 月(Month)
%d - 日(Day) 
%H - 小时(24-hour format)
%M - 分钟(Minute)
%S - 秒(Second)
%^ - 开启颜色
%$ - 关闭颜色

4. 日志记录器类

日志记录器类是 spdlog 中用于进行日志输出的类,内部定义了各种不同等级日志的输出接口,同时,也可以在该类中设置日志的输出格式以及输出级别等基础配置。

namespace spdlog {
    class logger {
        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);

        // 日志输出
        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);
    };
    
    logger->debug("{} 的年龄是: {}", "小徐", 21);
}

5. 异步日志记录类

为了异步记录日志,可以使用 spdlog::async_logger,其总体的常规操作与同步日志器并无区别,区别仅在于日志数据的写入方式有所不同,异步日志器的使用会伴随创建一个异步线程池 (默认只有一个线程) 进行日志的实际写入操作。

  • 同步:直接将日志写入磁盘,直到写完再继续向下执行。
  • 异步:通过 “生产者-消费者模型”,将日志 IO 从业务线程中解耦。业务线程只负责生成日志并入队,后台线程池负责格式化与持久化写入,从而显著降低延迟、提升吞吐量,但需要在性能与可靠性之间权衡队列大小与溢出策略。
class async_logger final : public logger {
    async_logger(std::string logger_name,
                sinks_init_list sinks_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::function<void()> on_thread_start);
        thread_pool(size_t q_max_items, size_t threads_n);
    };
}

// 默认线程池的初始化接口 
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");

6. 日志记录器工厂

日志器工厂是用于快速创建日志器对象的工厂类,spdlog 中默认提供了两种不同的日志器工厂类,一种是同步日志器工厂 (默认),一种是异步日志器工厂,工厂的选择通过日志器创建函数模板的模板参数进行设置。

using async_factory = async_factory_impl<async_overflow_policy::block>;

template<typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async(
    std::string logger_name, 
    SinkArgs &&...sink_args)

// 标准输出 
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> stdout_color_mt(
    const std::string &logger_name,
    color_mode mode = color_mode::automatic);
 
// 标准错误 
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)

//...

7. 日志落地类

日志落地类,负责决定将日志数据写入到哪些设备中,在 spdlog 中,实现了非常多的落地方向,这里贴出了标准输出,普通文件,滚动文件等几种常用对象的创建方式。

除此之外,还可以在该类中进一步细化设置日志的输出格式,输出等级等配置 (或者说其实 spdlog 中有很多地方都可以设置日志输出等级和输出格式,但是实际都会归总到日志落地类中进行最后的日志格式组织,以及判断是否该输出等操作)

namespace spdlog {
    namespace sinks {
        class SPDLOG_API sink_ptr
        {
        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;
            void set_level(level::level_enum log_level);
        };

        // 标准输出日志落地类 
        using stdout_sink_mt = stdout_sink<std::mutex>;
        using stderr_sink_mt = stderr_sink<std::mutex>;
        using stdout_color_sink_mt = stdout_color_sink<std::mutex>;
        using stderr_color_sink_mt = stderr_color_sink<std::mutex>;

        // 滚动日志文件-超过一定大小则自动重新创建新的日志文件 
        sink_ptr rotating_file_sink(filename_t base_filename,
                                    std::size_t max_size,
                                    std::size_t max_files,
                                    bool rotate_on_open = false,
                                    const file_event_handlers &event_handlers = {});
        // 普通的文件落地对象创建接口 
        sink_ptr basic_file_sink(const filename_t &filename,
                                bool truncate = false,
                                const file_event_handlers &event_handlers = {});

        using rotating_file_sink_mt = rotating_file_sink<std::mutex>;
        using basic_file_sink_mt = basic_file_sink<std::mutex>;
        using kafka_sink_mt = kafka_sink<std::mutex>;
        using mongo_sink_mt = mongo_sink<std::mutex>;
        using tcp_sink_mt = tcp_sink<std::mutex>;
        using udp_sink_mt = udp_sink<std::mutex>;
        // 其他类型的sink... 
        // *_st:单线程版本,不用加锁,效率更高 
        // *_mt:多线程版本,用于多线程程序是线程安全的。
    }
}

8. 全局配置接口

可以通过一些 spdlog 命名空间下的全局接口,对全局的日志输出进行配置 (一旦配置则对全局生效)

// 输出等级设置接口
void set_level(level::level_enum log_level);
// 日志刷新策略-每隔N秒刷新一次 
void flush_every(std::chrono::seconds interval);
// 日志刷新策略-触发指定等级立即刷新 
void flush_on(level::level_enum log_level);

9. 记录日志

使用日志记录器记录不同级别的日志

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");

10. 使用样例

这里使用日志记录器工厂的方式输出日志。

10.1 目录结构

spdlog/
|-- makefile
|-- stdout.cc

10.2 项目构建

# makefile
main: main.cc
	g++ -o $@ $^ -std=c++17 -lspdlog -lfmt -lpthread

.PHONY: clean
clean:
	rm -f main

10.3 样例编写

// main.cc
#include <iostream>
#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>

int main(int argc, char* argv[])
{
    // 1.创建日志对象
    auto logger = spdlog::stdout_color_mt("stdout_logger"); // 标准输出(同步)
    // auto logger = spdlog::basic_logger_mt("file_logger", "./log.txt"); // 文件输出
    // auto logger = spdlog::rotating_logger_mt("file_logger", "./log.txt", 1024, 3); // 循环文件输出
    // auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("stdout_logger"); // 标准输出(异步)

    // 2.设置日志输出等级
    logger->set_level(spdlog::level::debug);

    // 3.设置日志输出格式
    logger->set_pattern("[%Y-%m-%d %H:%M:%S] [%^%l%$] %v");

    // 4.输出日志
    logger->debug("{}今年{}岁", "张三", 18);
    logger->info("{}今年{}岁", "李四", 20);
    logger->warn("{}今年{}岁", "王五", 22);
    logger->error("{}今年{}岁", "赵六", 24);
    logger->critical("{}今年{}岁", "钱七", 26);
    // for (int i = 0; i < 10000; i++) {
    //     logger->info("Hello World - {}", i);
    // }

    return 0;
}

在这里插入图片描述

五. spdlog 封装

1. 设计与实现

因为 spdlog 本身提供的日志输出操作无法输出错误位置的文件名和行号,对于我们排查错误的时候有所不利,因此进行二次封装,针对其的日志输出操作封装一套宏操作出来以便于输出文件名和行号。

  • 定义日志器配置结构体。
  • 定义全局日志器。
  • 定义全局日志器初始化接口。
  • 封装日志输出宏。

先创建 “微服务-脚手架” 仓库:

在这里插入图片描述

新建 feature/spdlog 分支:

在这里插入图片描述

克隆仓库到本地:

在这里插入图片描述

在这里插入图片描述

创建并切换 feature/spdlog 分支,在该分支下进行编码,目的是防止与其他开发者造成冲突,这里只有我一人进行开发,没有冲突:

在这里插入图片描述

// xzylog.h
// 防止头文件重复包含
#pragma once
// 声明头文件
#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 xzylog {
    // 声明日志配置结构体
    struct log_settings {
        bool async; // 是否启用异步日志
        int level; // 日志输出等级:1-debug、2-info、3-warn、4-error、6-off
        std::string format; // 日志输出格式:[%Y-%m-%d %H:%M:%S] [%-7l] %v
        std::string path; // 日志输出路径:stdout、file
    };
    // 声明全局日志对象
    extern std::shared_ptr<spdlog::logger> g_logger;
    // 声明全局日志器初始化接口
    extern void log_init(const log_settings& settings);
    // 封装日志输出宏
    #define FMT_PREFIX std::string("[{}:{}]: ")
    #define DBG(fmt, ...) xzylog::g_logger->debug(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
    #define INF(fmt, ...) xzylog::g_logger->info(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
    #define WRN(fmt, ...) xzylog::g_logger->warn(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
    #define ERR(fmt, ...) xzylog::g_logger->error(FMT_PREFIX + fmt, __FILE__, __LINE__, ##__VA_ARGS__)
}
// xzylog.cc
#include "xzylog.h"

namespace xzylog {
    // 定义全局日志对象
    std::shared_ptr<spdlog::logger> g_logger = nullptr;
    // 定义全局日志器初始化接口
    void log_init(const log_settings& settings) {
        // 1.判断日志类型 - async、sync
        // 2.判断输出目标 - stdout、file
        // 3.创建日志器
        if (settings.async == true) {
            if (settings.path == "stdout") {
                g_logger = spdlog::stdout_color_mt<spdlog::async_factory>("logger");
            } else {
                g_logger = spdlog::basic_logger_mt<spdlog::async_factory>("logger", settings.path);
            }
        } else {
            if (settings.path == "stdout") {
                g_logger = spdlog::stdout_color_mt("logger");
            } else {
                g_logger = spdlog::basic_logger_mt("logger", settings.path);
            }
        }
        // 4.设置日志等级
        g_logger->set_level(spdlog::level::level_enum(settings.level));
        // 5.设置日志格式
        g_logger->set_pattern(settings.format);
    }
}

2. 使用

2.1 目录结构

|-- microservices-scaffold
|   |-- example
|   |   |-- spdlog
|   |       |-- log_test.cc
|   |       |-- makefile
|   |-- source
|       |-- xzylog.cc
|       |-- xzylog.h

2.2 项目构建

# makefile
CFLAGS = -I ../../source -std=c++17
log_test: log_test.cc ../../source/xzylog.cc
	g++ -o $@ $^ $(CFLAGS) -lspdlog -lgflags -lfmt -pthread

.PHONY: clean
clean:
	rm -f log_test

2.3 代码编写

// log_test.cc
#include <xzylog.h>
#include <gflags/gflags.h>

// 1.通过gflags定义日志配置参数
DEFINE_bool(log_async, true, "是否启用异步日志");
DEFINE_int32(log_level, 1, "日志输出等级: 1-debug、2-info、3-warn、4-error、6-off");
DEFINE_string(log_format, "[%Y-%m-%d %H:%M:%S] [%-7l] %v", "日志输出格式");
DEFINE_string(log_path, "stdout", "日志输出路径");

int main(int argc, char* argv[]) 
{
    // 2.解析参数
    google::ParseCommandLineFlags(&argc, &argv, true);
    // 3.初始化日志器参数配置结构
    xzylog::log_settings settings = {
        .async = FLAGS_log_async,
        .level = FLAGS_log_level,
        .format = FLAGS_log_format,
        .path = FLAGS_log_path,
    };
    // 4.初始化日志器
    xzylog::log_init(settings);
    // 5.输出日志
    DBG("Hello World");
    DBG("{}今年{}岁", "张三", 18);
    INF("{}今年{}岁", "李四", 20);
    WRN("{}今年{}岁", "王五", 22);
    ERR("{}今年{}岁", "赵六", 24);

    return 0;
}

在这里插入图片描述

提交本地代码到远程仓库

在本地创建并切换为 develop 分支:

在这里插入图片描述

将本地的 feature/spdlog、develop 和远程仓库中的 feature/spdlog、develop 分支建立连接,并拉取最新代码:

在这里插入图片描述

切换为 feature/spdlog 分支,并提交代码到本地仓库:

在这里插入图片描述

将 develop 分支合并到 feature/spdlog 分支,再提交到远程仓库 (如果合并存在冲突直接在本地修改再 git push 即可,后面的将 feature/spdlog 分支合并到 develop 分支就不会冲突,而如果直接 git push 就可能其它开发者也 git push 了代码,再合并就会冲突,进而导致需要再 develop 分支上修改)

在这里插入图片描述

在这里插入图片描述

将 feature/spdlog 分支合并到 develop 分支:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐