spdlog源码分析

Posted CPP RUST编程实践与软件技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spdlog源码分析相关的知识,希望对你有一定的参考价值。

最近在学习envoy的源码时, 发现项目中使用了一个名为spdlog的开源日志库. 该库在github上Start数达到9.6K, 基本上是C++日志库事实上的王者了.

这里放一段spdlog的示例代码, 让大家对spdlog的使用有一个基本的认识:

#include "spdlog/spdlog.h"
#include "spdlog/cfg/env.h" // for loading levels from the environment variable

int main(int, char *[]){
   std::string key_val("very important message");
   spdlog::info("Welcome to spdlog version {}.{}.{} !", SPDLOG_VER_MAJOR, SPDLOG_VER_MINOR, SPDLOG_VER_PATCH);
   spdlog::warn("Easy padding in numbers like {:08d}, key_val={}", 12, key_val);
   spdlog::critical("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
   spdlog::info("Support for floats {:03.2f}", 1.23456);
   // 包含文件名, 函数和行号信息
   SPDLOG_DEBUG("Support for floats {:03.2f}", 1.23456);
}

可以看到, spdlog使用类似python的字符串格式化语法, 这也是C++20引入的std::format的语法. 接口还是非常简洁的.

本文将通过分析其源码, 看看它在哪些方面打动了用户.

我们需要什么样的日志库

在分析源码之前, 首先需要知道一个好的日志库需要考虑哪些问题. 这里通过如下两个方面进行分析:

  1. 日志的生产. 指业务代码如何日志接口打印日志. 这里的关键是日志接口的设计.

  2. 日志的消费. 日志通过日志接口搜集之后, 需要送到相关的目的地显示或者保存.

日志的生产

日志的生产部分, 决定了我们如何使用日志库, 也就是日志库有什么样的接口, 接口的协议规约是怎么样的.

目前, 日志库一般有三种风格的打印日志的接口:

  1. 类printf形式.

    int main(int argc, char* argv[]){
       std::string message("important message");
    log_init(LL_TRACE, "mysql", "./log/");
    LOG_NOTICE("%s [time:%d]", "test calling log", time(NULL));
    LOG_DEBUG("debug msg,only write to log [time:%d], [%s]", time(NULL), message.c_str());
    LOG_WARN("warnning msg will be writing to the error files [time:%d]", time(NULL));
    LOG_ERROR("you also can change number of output files by rewrite the macro_define.h");
       return 0;
    }
  2. 类C++ stream形式. 可以看到, 虽然不用指定类型, 但是其实比方式1更加繁琐.

    // google glog
    #include <glog/logging.h>
    int main(int argc, char* argv[]) {
       // Initialize Google's logging library.
       string val = "key information"
       google::InitGoogleLogging(argv[0]);
       LOG(INFO) << "Found " << num_cookies << " cookies";
       LOG(WARNING) << "thisis the 1st warning!" << val;
       return 0;
    }
  3. 类python形式. 即spdlog使用的方式, 示例参考文章开头.

这三种形式的接口对比如下:


类printf形式 类C++ stream形式 类python形式
类型安全 不安全 安全 安全
易用性 格式化串需要显示指定类型使用繁琐, 比如经常要输出proto的message 输出多个变量时很繁琐 易用
扩展性 对非基本类型不可扩展 可扩展 可扩展

这里对易用性做一个说明, 普通的例子可能不明显, 但是在分布式系统中, 有时候需要输出protobuf的Message的内容, 使用这三种形式输出日志的对比如下:

// 三种形式对比:
int getUser(const google::protobuf::Message& req, google::protobuf::Message& resp){
   int ret = 0;
   ret = Rpc1(req, resp);
   if(ret){
       // 类printf形式
       LOG_ERROR("ret=%d|req=%s|resp=%s", ret, req.Utf8DebugString().c_str(), resp.Utf8DebugString().c_str());
       // 类C++ Stream形式
LOG(ERROR) << "Rpc1|ret=" << ret << "|req=" << req << "|resp=" << resp;
       // 类python形式
       SPDLOG_ERROR("Rpc1|ret={}|req={}|resp={}", req, req, resp);
  }
   return 0;
}

笔者所在的公司使用的是类printf形式, 打日志写Utf8DebugString().c_str()和string类型的.c_str()写到吐血.

日志的消费

为了定位问题, 开发过程中一般使用控制台查看日志, 而在生产环境, 则需要将日志持久化到文件或者通过网络传送到专门的日志系统中进行存储和检索. 一般的日志库均支持:

  1. 控制台输出日志内容. 并可以根据日志级别的不同显示不同颜色, 比如, 高亮或红色显示错误日志.

  2. 将日志输出到文件. 输出到文件一般支持多种策略, 比如按小时, 按天分割文件, 删除太久之前的历史日志文件.

  3. 将日志输出到远程日志系统.

spdlog的架构和源码

聊完了我们对日志库的需求, 接下来就开始分析spdlog的源码.

spdlog的源码, 从功能上主要分成四大部分.

第一部分是面向业务的日志输出接口. 业务通过这些接口打印日志, 这部分又分为宏接口和全局函数接口. 区别是, 宏接口会包含文件名和行号/函数名信息. 关键接口列表如下:

  • 宏接口(日志信息包含文件名/行号信息)

    • SPDLOG_TRACE

    • SPDLOG_DEBUG

    • SPDLOG_INFO

    • SPDLOG_WARN

    • SPDLOG_ERROR

    • SPDLOG_CRITICAL

  • 全局函数

    • spdlog::trace

    • spdlog::debug

    • spdlog::info

    • spdlog::warn

    • spdlog::error

    • spdlog::critical

第二部分为日志格式化部分. 日志格式化部分根据预定义的pattern对日志内容进行格式化, 然后送给下游进行存储或者展示.

预定义的pattern类似下面的格式:

  [%H:%M:%S %z] [%^%L%$] [thread %t] %v

每个字段均代表一个预定义的字段. 所有支持的字段可以参考这里.

第三部分为异步日志输出设施. 这部分主要由线程池/日志信息队列. 日志在业务线程产生之后, 就插入消息队列, 通过线程池中的线程不断消费, 循环调用sink部分, 将日志写入目的地.

第四部分为sink部分. sink部分负责将日志写入文件/终端/网络. 一个日志可以同时写入不同的sink, 满足多输出的要求.

下图为各个部分的关系. 黄色线条为异步的日志流向, 即日志的产生和处理在不同的线程进行, 最小化日志对业务的影响. 蓝色线条为同步日志流向, 日志产生并写入目的地之后才会返回业务逻辑代码.

spdlog的整体框架图如下:

日志格式化部分

日志格式化部分的设计充分践行了软件设计中的单一职责原则. 在spdlog中, 每一条日志包含什么内容/如何输出均通过格式化串指定, 例如如下的格式化串: [%H:%M] %v. 将被拆分成7个flag_formatter进行处理. 分成4个原始串formatter和小时(%H), 分钟(%M), 用户串(%v). 每一个部分的处理均由一个类来处理.

每一个格式化串均会被编译成flag_formatter列表, 虽然这里会有不少的虚函数调用开销, 但是代码的可维护性大大提升.

这里也支持业务方自定义flag的处理类.  满足用户自定义需求. 示例代码可以参考这里.





以上是关于spdlog源码分析的主要内容,如果未能解决你的问题,请参考以下文章

xcode使用spdlog(1.7)总结

c++ 日志输出库 spdlog 简介

Android 插件化VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )(代码片段

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段

如何在代码中启用/禁用 spdlog 日志记录?