printf 源码_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 variableint 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 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的整体框架图如下:

8840cb0a06293aa26a311eaae990baf4.png

日志格式化部分

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

e959e6e71807b87ff3c23e9c48133a60.png

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值