spdlog库学习(三):运行

系列文章目录


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 系列文章目录
  • example运行:
  • bench_mark运行
  • 简单代码分析
    • 普通用法
    • default_logger_raw()
    • registry/logger
      • logger::log(...)的重载
    • sink
    • 类图
    • 编译时日志等级


example运行:

示例中的程序,可以通过教程简单的编译出来

$ git clone https://github.com/gabime/spdlog.git
$ cd spdlog && mkdir build && cd build
$ cmake .. && make -j

执行结果如下:
输出好像并没有自带代码行号,不过应该可以自行配置吧
spdlog库学习(三):运行_第1张图片

bench_mark运行

不知为何,bench目录中的 CMakelist.txt不能直接使用。通过先安装这个库,再使用g++编译运行

$ sudo make install 
$ g++ latency.cpp -lspdlog -lpthread -o latency

以下四个文件,有两个文件因为缺少头文件编译出错

  • async_bench.cpp
  • bench.cpp
  • formatter-bench.cpp
  • latency.cpp
?─? g++ formatter-bench.cpp -lspdlog -lpthread -o formatter-bench
formatter-bench.cpp:6:10: fatal error: benchmark/benchmark.h: No such file or directory
    6 | #include "benchmark/benchmark.h"
      |          ^~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
?─ ~/learn/spdlog/bench v1.x ?3                                                                                                                                                                                                                            ?  gyl@mi 22:47:01
?─? g++ latency.cpp -lspdlog -lpthread -o latency 
latency.cpp:10:10: fatal error: benchmark/benchmark.h: No such file or directory
   10 | #include "benchmark/benchmark.h"
      |          ^~~~~~~~~~~~~~~~~~~~~~~
      
compilation terminated.

另外两个程序运行如下

  • bench
    spdlog库学习(三):运行_第2张图片

  • async_bench
    该方法用于估计自己程序中的效率。以如下参数运行该程序,结果如下
    spdlog库学习(三):运行_第3张图片

简单代码分析

#include "spdlog/spdlog.h"

int main() 
{   // 直接输出字符串 
    spdlog::info("Welcome to spdlog!");
    // 支持多种格式化模板
    // 各种排版参数
    spdlog::error("Some error message with arg: {}", 1);
    spdlog::warn("Easy padding in numbers like {:08d}", 12);
    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::info("Positional args are {1} {0}..", "too", "supported");// 定制顺序
    spdlog::info("{:<30}", "left aligned");// 排版格式
    
    //运行时log等级
    spdlog::set_level(spdlog::level::debug); // Set global log level to debug
    spdlog::debug("This message should be displayed..");    
    
    // 改变日志格式change log pattern
    spdlog::set_pattern("[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v");
    
    // 编译时设置日志级别
    // define SPDLOG_ACTIVE_LEVEL to desired level
    SPDLOG_TRACE("Some trace message with param {}", 42);
    SPDLOG_DEBUG("Some debug message");
}

普通用法

除了trace以外,所有日志等级的实现方式都差不多一样,这里以**info(…)**为例来看它的调用栈。
示例代码中展示了两种输出日志的方式:字符串输出 和 带格式化参数的输出,对应代码如下:

**info(…)**实现在spdlog.h
spdlog库学习(三):运行_第4张图片
知识点:

  • 除了普通的格式化字符串,还提供了宽字符的格式化字符串wformat_string。
  • 使用可变参数:fmt是一个用于格式化的对象。该类还是一个模板,高级语法,需要仔细研究下实现
  • 使用完美转发:将变参传递给logger

default_logger_raw()

**default_logger_raw()**实现在spdlog-inl.h,返回了一个logger的裸指针。所以spdlog库的组织是 xxx.h是头文件,xxx-inl.h是实现文件。具体为什么要这么组织,我觉得得等自己尝试实现的时候,才能够体会到。
在这里插入图片描述
default_logger_raw()返回了一个裸指针。没有使用智能指针,应该是为了效率,而且仅供内部使用。

registry是实现在details命名空间的类,而且是一个单例,使用时直接调用静态函数instance()。
定义在spdlog/details/registry.h
实现在spdlog/details/registry-inl.h
在这里插入图片描述
插一句题外话,c++11 要求类内静态函数要线程安全,所以c++11以后实现单例模式,这就是标准做法。C++函数内的静态变量初始化以及线程安全问题

registry/logger

**get_default_raw()**返回一个registry对象内部保存的logger对象。
spdlog库学习(三):运行_第5张图片

根据注释的描述:返回默认logger的裸指针,是给默认的api使用,效率更高,但是不能和set_default_logger()一起使用。也就是说,不要在不同的线程分别调用set_defautl_logger()和std::info()等默认的日志输出api

所以,info最终是调用了logger::info()函数
logger实现在spdlog/logger.h

logger::log(…)的重载

info的实现有三个重载,对应了前面在spdlog.h中的三个全局的重载


    template
    void info(format_string_t fmt, Args &&... args)
    {
        log(level::info, fmt, std::forward(args)...);
    }
    
    template
    void info(wformat_string_t fmt, Args &&... args)
    {
        log(level::info, fmt, std::forward(args)...);
    }
    template
    void info(const T &msg)
    {
        log(level::info, msg);
    }

上面代码可以看到,log的第一个参数是日志等级,后面的参数跟输出的信息有关。还是比较简单的。但是log函数实际上有大量重载。其变化主要出在头两个参数。一个time_point类型和一个source_loc类型,看名字就知道一个是时间戳,一个是输出日志的源码位置。后续要看一下这两个类的实现

log(…)的重载有很多:

void log(source_loc loc, level::level_enum lvl, format_string_t fmt, Args &&... args)
**void log(level::level_enum lvl, format_string_t fmt, Args &&... args)**
**void log(level::level_enum lvl, const T &msg)**
void log(source_loc loc, level::level_enum lvl, const T &msg)
void log(log_clock::time_point log_time, source_loc loc, level::level_enum lvl, string_view_t msg)
void log(source_loc loc, level::level_enum lvl, string_view_t msg)
void log(level::level_enum lvl, string_view_t msg)
void log(source_loc loc, level::level_enum lvl, wformat_string_t fmt, Args &&... args)
**void log(level::level_enum lvl, wformat_string_t fmt, Args &&... args)**
void log(log_clock::time_point log_time, source_loc loc, level::level_enum lvl, wstring_view_t msg)
void log(source_loc loc, level::level_enum lvl, wstring_view_t msg)
void log(level::level_enum lvl, wstring_view_t msg)

以**void log(level::level_enum lvl, format_string_t fmt, Args &&… args)**调用情况为例:

template
void log(level::level_enum lvl, format_string_t fmt, Args &&... args)
{
    log(source_loc{}, lvl, fmt, std::forward(args)...);
}
template
void log(source_loc loc, level::level_enum lvl, format_string_t fmt, Args &&... args)
{
    log_(loc, lvl, fmt, std::forward(args)...);
}

可以看到又调用了带source_loc的重载版本。
带source_loc的版本又调用了log_(…)
log_的重载倒是不多,只有在开启SPDLOG_WCHAR_TO_UTF8_SUPPORT宏之后,会重载两个,所以共有三个。正常的一个是

    // common implementation for after templated public api has been resolved
    template
    void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&... args)
    {
        **bool log_enabled = should_log(lvl);**
        **bool traceback_enabled = tracer_.enabled();**
        if (!log_enabled && !traceback_enabled)
        {
            return;
        }
        SPDLOG_TRY
        {
            memory_buf_t buf;
#ifdef SPDLOG_USE_STD_FORMAT
            **fmt_lib::vformat_to(std::back_inserter(buf), fmt, fmt_lib::make_format_args(std::forward(args)...));**
#else
            // seems that fmt::detail::vformat_to(buf, ...) is ~20ns faster than fmt::vformat_to(std::back_inserter(buf),..)
            fmt::detail::vformat_to(buf, fmt, fmt::make_format_args(std::forward(args)...));
#endif

            details::log_msg log_msg(loc, name_, lvl, string_view_t(buf.data(), buf.size()));
            **log_it_(log_msg, log_enabled, traceback_enabled);**
        }
        SPDLOG_LOGGER_CATCH(loc)
    }

log_(…) 函数只是判断了该日志等级是否需要输出,以及是否需要输出trace。然后调用fmt_lib生成了最后需要输出的msg,然后调用log_it(…)进行输出
log_it_实现在logger-inl.h

SPDLOG_INLINE void logger::log_it_(const spdlog::details::log_msg &log_msg, bool log_enabled, bool traceback_enabled)
{
    if (log_enabled)
    {
        sink_it_(log_msg);
    }
    if (traceback_enabled)
    {
        tracer_.push_back(log_msg);
    }
}

sink

其中首先调用sink_it_输出到sinks,然后调用tracer_.push_back()将消息输出到环形缓冲区。
sink_it_会遍历所有的sink,然后调用sink的should_log(…)判断是否需要输出,以及sink::log(…),将日志输出到目标。
这里意识到,logger有自己的日志等级,sinks也有自己的日志等级。而logger的日志等级优先级要高一些,因为它优先判断。

SPDLOG_INLINE void logger::sink_it_(const details::log_msg &msg)
{
    for (auto &sink : sinks_)
    {
        **if (sink->should_log(msg.level))**
        {
            SPDLOG_TRY
            {
                **sink->log(msg);**
            }
            SPDLOG_LOGGER_CATCH(msg.source)
        }
    }

    if (should_flush_(msg))
    {
        flush_();
    }
}

默认的logger,内部的sink是stdout_sink_base类型,实现在sinks\stdout_sinks-inl.h

template
SPDLOG_INLINE void stdout_sink_base::log(const details::log_msg &msg)
{
  // 删除了windows实现
    std::lock_guard lock(mutex_);
    memory_buf_t formatted;
    **formatter_->format(msg, formatted);**
    **::fwrite(formatted.data(), sizeof(char), formatted.size(), file_);//file_实际上是stdout或者stderr**
    ::fflush(file_); // flush every line to terminal
}

可以看到这里又格式化了一遍,最后才调用fwrite写到
sink->log()往往是调用fwrite系统调用,将日志输出到FILE*

再回到log_it_(…)中的trace。trace_是logger中的一个属性。该类实现在backtracer.h。其内部是由一个循环数组实现的。

class logger{
...
	details::backtracer tracer_;
...
};
class SPDLOG_API backtracer
{
...
    circular_q messages_; //底层是vector实现的
...
};

至此,输出到控制台的整个流程就屡顺了。其他输出到文件也应该都差不多。只有异步的地方还需要再屡一下。接着就抠一些细节了。

类图

只是画出了至此的类图,其中有不完整的地方。但是该类图好懂,可以在脑中搭建一个初级框架,之后再往里面填东西,就容易理解的多。

spdlog库学习(三):运行_第6张图片

编译时日志等级

由宏实现,从这里大致也可以猜到是用宏的大小比较,决定在编译时将宏映射为输出还是不输出。从这里也可以看到日志库的日志等级安排。
SPDLOG_ACTIVE_LEVEL
设置日志等级:#define define SPDLOG_ACTIVE_LEVEL
其内部实现,还是用了 default_logger_raw()
根据实现,所有>=该设置的日志等级,都会被输出。
spdlog库学习(三):运行_第7张图片
日志等级由低到高分别是:

// SPDLOG_LEVEL_TRACE,
// SPDLOG_LEVEL_DEBUG,
// SPDLOG_LEVEL_INFO,
// SPDLOG_LEVEL_WARN,
// SPDLOG_LEVEL_ERROR,
// SPDLOG_LEVEL_CRITICAL,
上面6个应该是大多数日志库用的日志等级排序,程序员最好记住。起码要知道,当发现写的日志打印不出来时,怎么调高一个等级输出。
CRITICAL是最高的日志等级,INFO比DEBUG的日志等级要高。
// SPDLOG_LEVEL_OFF 是为了关闭所有日志而存在的,

以trace来看宏的嵌套调用

spdlog库学习(三):运行_第8张图片

  • 两种宏,一种可以自定义logger,一种使用默认logger。
  • SPDLOG_LOGGER_CALL调用了logger的日志输出。
  • source_loc用于输出代码的文件名、行号、函数名等信息。
  • SPDLOG_FUNCTION 将__FUNCTION__强制转换为 const char* ,不知道为什么
    在这里插入图片描述

你可能感兴趣的:(开源库学习,学习历程,学习)