异步日志方案——spdlog

本文涉及的产品
图片翻译,图片翻译 100张
文档翻译,文档翻译 1千页
日志服务 SLS,月写入数据量 50GB 1个月
简介: 异步日志方案——spdlog

日志的作用

在程序运行过程中打印日志,可以帮助我们跟踪调试程序的运行状态,获取需要的调试信息。通过日志打印的时间,我们可以了解程序在特定时间的行为,分析性能瓶颈,甚至有大佬可以预测潜在的系统故障。

什么是spdlog

spdlog是一款开源的C++日志库,他有着极高的性能和几乎零成本的抽象。spdlog支持异步和同步日志记录,提供了多种日志级别,可以将日志打印在终端,文件或者自定义接收器(如数据库)。

git clone https://github.com/gabime/spdlog.git
cd spdlog
mkdir build & cd build
cmake ..
make
sudo make install

spdlog为什么高效

  • 零成本的抽象:spdlog通过模版和内联函数,确保只有在真正需要的时候才进行日志记录。(内联函数在需要的时候会直接编译在程序中,模版只有在我们需要的时候才会展开)
  • 异步日志记录:spdlog支持异步日志记录,这意味着他可以将消息发送到线程池进行处理,不会阻塞当前线程,减少了主线程的开销。
  • 高效的格式化:spdlog使用fmt库进行高效的字符串格式化,减少格式化的时间。

spdlog的特征

  • 高速的日志记录:spdlog可以每秒处理百万条日志消息。
  • 低内存占用:spdlog的设计确保在高负载下依然保持低内存占用。
  • 灵活的配置:用户可以根据需要配置spdlog,选择异步(mt)或者同步(st),选择不同的日志级别和输出目标。

spdlog的输出控制

  • 多种日志级别:spdlog的日志级别包括:trace,debug,info,warn,error和critical。
  • 多目标:可以将日志输出到终端,文件和接收器(数据库、远程服务器等)。
  • 格式化输出:fmt支持高调的格式化输出

spdlog处理流程

spdlog的不同组件负责不同的部分。registry负责管理所有的组件,logger负责记录日志,sink负责将日志输出到指定位置,formatter将日志转化为特定格式,Async Logger可以异步将日志消息写入sink。

spdlog使用方法

使用默认logger打印输出

#include <chrono>
#include <iostream>
#include <memory>
#include <spdlog/async.h>
#include <spdlog/async_logger.h>
#include <spdlog/common.h>
#include <spdlog/logger.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
int main()
{
    spdlog::trace("hello trace");
    spdlog::debug("hello debug");
    spdlog::info("hello info");
    spdlog::warn("hello warn");
    spdlog::error("hello error");
    spdlog::critical("hello critical");
    return 0;
}

我们查看这些接口的源代码实现如下:

template <typename T>
inline void trace(const T &msg) {
    default_logger_raw()->trace(msg);
}
template <typename T>
inline void debug(const T &msg) {
    default_logger_raw()->debug(msg);
}
template <typename T>
inline void info(const T &msg) {
    default_logger_raw()->info(msg);
}
template <typename T>
inline void warn(const T &msg) {
    default_logger_raw()->warn(msg);
}
template <typename T>
inline void error(const T &msg) {
    default_logger_raw()->error(msg);
}
template <typename T>
inline void critical(const T &msg) {
    default_logger_raw()->critical(msg);
}

我们上面说过,logger是负责记录日志消息的,在spdlog中存在一个默认logger可以直接使用,不需要我们进行配置。通过上图的输出可以看出来:默认的logger只会输出info级别及其以上级别的日志。

通过工厂模式接口配置logger

int main()
{
    auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("console");
    logger->set_pattern("***[%H:%M:%S %z][thread %t] %v***");
    logger->info("logger info");
    spdlog::get("console");
    spdlog::get("console")->error("logger error");
    return 0;
}

这边我们自己定义了一个logger,对他进行了输出的格式化,并且将已经打印的日志get出来,重新打印输出。这里之所以可以get出来是因为:从上面的结构图可以看出,logger是归registry管理的,我们通过单例registry将logger找到,换句话说,每一个logger在创建时,都是向registry注册。

源码接口:

namespace spdlog {
template <typename Factory>
SPDLOG_INLINE std::shared_ptr<logger> stdout_color_mt(const std::string &logger_name,
                                                      color_mode mode) {
    return Factory::template create<sinks::stdout_color_sink_mt>(logger_name, mode);
}
template <typename Factory>
SPDLOG_INLINE std::shared_ptr<logger> stdout_color_st(const std::string &logger_name,
                                                      color_mode mode) {
    return Factory::template create<sinks::stdout_color_sink_st>(logger_name, mode);
}
template <typename Factory>
SPDLOG_INLINE std::shared_ptr<logger> stderr_color_mt(const std::string &logger_name,
                                                      color_mode mode) {
    return Factory::template create<sinks::stderr_color_sink_mt>(logger_name, mode);
}
template <typename Factory>
SPDLOG_INLINE std::shared_ptr<logger> stderr_color_st(const std::string &logger_name,
                                                      color_mode mode) {
    return Factory::template create<sinks::stderr_color_sink_st>(logger_name, mode);
}
}
//这里是通过工厂模式的接口,有mt(异步)和st(同步)的接口。
SPDLOG_INLINE std::shared_ptr<logger> get(const std::string &name) {
    return details::registry::instance().get(name);
}
//从这里可以看出,单例registry可以get到logger

配置格式化打印(logger层面)

logger->set_pattern("***[%H:%M:%S %z][thread %t] %v***");
• 1

这一行就是配置格式化打印,配置方法可以参考文档中的表格:

配置输出位置sink & 配置格式化打印(sink层面)

class my_formatter_flag : public spdlog::custom_flag_formatter
{
public:
    void format(const spdlog::details::log_msg &, const std::tm &, spdlog::memory_buf_t &dest) override
    {
        std::string some_text = "lenn-flag";
        dest.append(some_text.data(), some_text.data() + some_text.size());
    }
    std::unique_ptr<spdlog::custom_flag_formatter> clone() const override
    {
        return spdlog::details::make_unique<my_formatter_flag>();
    }
};
int main()
{
    auto logger1 = std::make_shared<spdlog::logger>(std::string("console1"));
    auto sink1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    auto sink2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("lenn.log");
    auto formatter = std::make_unique<spdlog::pattern_formatter>();
    formatter->add_flag<my_formatter_flag>('*').set_pattern("[%n] [%*] [%^%l%$] %v");
    sink1->set_formatter(std::move(formatter));
    sink2->set_pattern("[%^%l%$] %v");
    logger1->sinks().push_back(sink1);
    logger1->sinks().push_back(sink2);
    spdlog::register_logger(logger1);
    spdlog::get("console1")->info("sink demoe");
    
    return 0;
}

这里我们配置了两个sink,一个是输出到终端(stdout)还有一个输出到文件(basic_file)。上面一种通过工厂接口配置logger只能有一个sink,但是我们这里手动配置logger就可以有多个sink。

我们还可以定制标志,通过继承spdlog::custom_flag_formatter来定义自己的flag,比如我这里定义了一个叫[lenn-flag]的flag。

这里我们对两个sink分别设置输出格式,而不是对logger设置格式让logger管理下的所有sink的格式都一样;当人你也可以对logger直接set_pattern来使这个logger的所有输出格式都是一样的。

sink的选取

关于sink的选取,我们可以在源代码中看到:

这些sinks都是可以使用的,有android,qt,console等等,输出到不同地方的sinks。大家可以参考文档自行选取使用。

显示日志所在行号

#include <chrono>
#include <iostream>
#include <memory>
#include <spdlog/async.h>
#include <spdlog/async_logger.h>
#include <spdlog/common.h>
#include <spdlog/logger.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
class my_formatter_flag : public spdlog::custom_flag_formatter
{
public:
    void format(const spdlog::details::log_msg &, const std::tm &, spdlog::memory_buf_t &dest) override
    {
        std::string some_text = "lenn-flag";
        dest.append(some_text.data(), some_text.data() + some_text.size());
    }
    std::unique_ptr<spdlog::custom_flag_formatter> clone() const override
    {
        return spdlog::details::make_unique<my_formatter_flag>();
    }
};
int main()
{
    auto logger1 = std::make_shared<spdlog::logger>(std::string("console1"));
    auto sink1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    auto sink2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("lenn.log");
    auto formatter = std::make_unique<spdlog::pattern_formatter>();
    formatter->add_flag<my_formatter_flag>('*').set_pattern("[%n] [%*] [%^%l%$] %v");
    sink1->set_formatter(std::move(formatter));
    sink2->set_pattern("[%^%l%$] %v");
    logger1->sinks().push_back(sink1);
    logger1->sinks().push_back(sink2);
    spdlog::register_logger(logger1);
    spdlog::get("console1")->info("sink demoe");
    //这里可以显示行号
    SPDLOG_INFO("hello line");
    SPDLOG_LOGGER_INFO(logger1, "hello logger line");
    
    return 0;
}

SPDLOG_INFO宏在spdlog库中的实现使用了预定义的宏__FILE__、__LINE__和__func__(编译器的行为),这些预定义宏可以在编译时获取当前源文件名、行号和函数名。当你使用SPDLOG_INFO宏输出日志消息时,内部会自动将当前的文件名、行号和函数名信息加入到输出的日志消息中。这样就能够方便地追踪日志发生的位置。

线程池处理

int main()
{
    auto sink1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    auto sink2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("lenn.log");
    //队列中最多有8292条日志消息,有8个子线程打印输出
    spdlog::init_thread_pool(8292, 8);
    std::vector<spdlog::sink_ptr> sinks{sink1, sink2};
    auto logger_tp = std::make_shared<spdlog::async_logger>("tp", sinks.begin(), sinks.end(),//传入vector的迭代器即可
        spdlog::thread_pool(), spdlog::async_overflow_policy::overrun_oldest);
    
    logger_tp->info("hello thread pool");
    logger_tp->flush();//强制刷星所有注册的sink,确保日志消息立刻写入目标输出
    logger_tp->flush_on(spdlog::level::err);//遇到error级别的日志立刻刷新,缺号尽快得到error级别的日志
    spdlog::flush_every(std::chrono::seconds(5));//每5s刷新一次,将缓存的日志写入目标
    return 0;
}

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3月前
|
存储 监控 Serverless
阿里泛日志设计与实践问题之Grafana Loki在日志查询方案中存在哪些设计限制,如何解决
阿里泛日志设计与实践问题之Grafana Loki在日志查询方案中存在哪些设计限制,如何解决
|
20天前
|
消息中间件 存储 监控
微服务日志监控的挑战及应对方案
【10月更文挑战第23天】微服务化带来模块独立与快速扩展,但也使得日志监控复杂。日志作用包括业务记录、异常追踪和性能定位。
|
2月前
|
Kubernetes API Docker
跟着iLogtail学习容器运行时与K8s下日志采集方案
iLogtail 作为开源可观测数据采集器,对 Kubernetes 环境下日志采集有着非常好的支持,本文跟随 iLogtail 的脚步,了解容器运行时与 K8s 下日志数据采集原理。
|
3月前
|
XML 监控 Java
异步日志:性能优化的金钥匙
本文主要介绍了Log4j2框架的核心原理、实践应用以及一些实用的小Tips,力图揭示Log4j2这一强大日志记录工具在现代分布式服务架构运维中的关键作用。
|
3月前
|
SQL JavaScript 前端开发
【Azure 应用服务】Azure JS Function 异步方法中执行SQL查询后,Callback函数中日志无法输出问题
【Azure 应用服务】Azure JS Function 异步方法中执行SQL查询后,Callback函数中日志无法输出问题
|
3月前
|
存储 Prometheus Kubernetes
在K8S中,如何收集K8S日志?有哪些方案?
在K8S中,如何收集K8S日志?有哪些方案?
|
3月前
|
存储 Kubernetes Java
阿里泛日志设计与实践问题之在写多查少的降本场景下,通过SLS Scan方案降低成本,如何实现
阿里泛日志设计与实践问题之在写多查少的降本场景下,通过SLS Scan方案降低成本,如何实现
|
4月前
|
Java API Apache
通用快照方案问题之Feign对日志的记录如何解决
通用快照方案问题之Feign对日志的记录如何解决
29 0
|
4月前
|
存储 运维 监控
在Spring Boot中集成分布式日志收集方案
在Spring Boot中集成分布式日志收集方案
|
14天前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
123 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板