提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
示例中的程序,可以通过教程简单的编译出来
$ git clone https://github.com/gabime/spdlog.git
$ cd spdlog && mkdir build && cd build
$ cmake .. && make -j
执行结果如下:
输出好像并没有自带代码行号,不过应该可以自行配置吧
不知为何,bench目录中的 CMakelist.txt不能直接使用。通过先安装这个库,再使用g++编译运行
$ sudo make install
$ g++ latency.cpp -lspdlog -lpthread -o latency
以下四个文件,有两个文件因为缺少头文件编译出错
?─? 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.
另外两个程序运行如下
#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(…)**为例来看它的调用栈。
示例代码中展示了两种输出日志的方式:字符串输出 和 带格式化参数的输出,对应代码如下:
**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++函数内的静态变量初始化以及线程安全问题
**get_default_raw()**返回一个registry对象内部保存的logger对象。
根据注释的描述:返回默认logger的裸指针,是给默认的api使用,效率更高,但是不能和set_default_logger()一起使用。也就是说,不要在不同的线程分别调用set_defautl_logger()和std::info()等默认的日志输出api
所以,info最终是调用了logger::info()函数
logger实现在spdlog/logger.h
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
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_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_ACTIVE_LEVEL
设置日志等级:#define define SPDLOG_ACTIVE_LEVEL
其内部实现,还是用了 default_logger_raw()
根据实现,所有>=该设置的日志等级,都会被输出。
日志等级由低到高分别是:
// 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来看宏的嵌套调用