spdlog源码分析:高性能日志库设计原理 #
引言 #
spdlog是一个广受欢迎的C++日志库,以其高性能和灵活性著称。本文深入分析其源码实现。
架构 #
如图,其实也不只是spdlog,几乎所有的Log模块都是这样的架构:
- Macro:Log的宏,主要用于封装Logger的函数调用,通过宏可以记录下来当前的文件名和行号等信息,然后可以当做参数传递到函数中。
- Logger:Logger对象作为一个组合体,组合Format和Sink,内部会先调用Format,将Format后的Msg调用Sink,写到某个地方或某些地方。
- Resolve:Resolve是个函数,它的主要功能就是把用户输入的信息拼接成字符串:
- 用户可能有流式输出,比如
std::cout << 1 << "a" << 1;
,那拼接后的字符串就是1a1 - 用户可能有python式输出,比如
printf("{}你好{}", 1, 2);
那拼接后的字符串就是1你好2。
- 用户可能有流式输出,比如
- Format:用于格式化日志信息,只打印用户输入的信息还不够,我们还需要附加行号、文件名、进程ID、线程ID、时间戳、日志Level等信息,这样才方便根据日志分析问题。
- 比如用户打印的日志是’你好SPDLOG’,那日志中可能输出的是
- 2024-03-28 10:01:01 PID(123) TID(234) [INFO] hello_world.cc:123 你好SPDLOG
- 当然,我们可以自定义Format格式,实现自己的需求。
- 比如用户打印的日志是’你好SPDLOG’,那日志中可能输出的是
- Sink:用于控制日志写入的位置,比如写到控制台、写到文件、写到Logcat等等地方,我们也可以自定义写入的位置,比如Sink到某个网络服务器上,也可以一份日志输出到多个位置。
了解这个架构后,我们直接去源码中找各个模块对应的位置就可以。
Log的宏在哪里? #
#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_INFO
#define SPDLOG_LOGGER_INFO(logger, ...) \
SPDLOG_LOGGER_CALL(logger, spdlog::level::info, __VA_ARGS__)
#define SPDLOG_INFO(...) SPDLOG_LOGGER_INFO(spdlog::default_logger_raw(), __VA_ARGS__)
#else
#define SPDLOG_LOGGER_INFO(logger, ...) (void)0
#define SPDLOG_INFO(...) (void)0
#endif
这里可以看到 SPDLOG_INFO的宏,会转到SPDLOG_LOGGER_CALL宏上,并且携带了spdlog::default_logger_raw() 和 日志的info级别,再看SPDLOG_LOGGER_CALL的定义:
#ifndef SPDLOG_NO_SOURCE_LOC
#define SPDLOG_LOGGER_CALL(logger, level, ...) \
(logger)->log(spdlog::source_loc{__FILE__, __LINE__, SPDLOG_FUNCTION}, level, __VA_ARGS__)
#else
#define SPDLOG_LOGGER_CALL(logger, level, ...) \
(logger)->log(spdlog::source_loc{}, level, __VA_ARGS__)
#endif
通过SPDLOG_LOGGER_CALL的定义,可以知道SPDLOG_INFO,最终会调用 spdlog::default_logger_raw()->log(spdlog::source_loc{__FILE__, __LINE__, SPDLOG_FUNCTION}, spdlog::level::info, __VA_ARGS__)
那spdlog::default_logger_raw()是什么?了解上面的架构后,我们肯定知道,这个就是架构中的Logger对象。此处代码详见:spdlog.h#L323
Logger对象源码在哪里? #
为方便理解,我删减了大部分代码,保留关键部分:
class SPDLOG_API logger {
public:
// Logger with sinks init list
logger(std::string name, sinks_init_list sinks)
: logger(std::move(name), sinks.begin(), sinks.end()) {}
void set_formatter(std::unique_ptr<formatter> f);
protected:
std::string name_;
std::vector<sink_ptr> sinks_;
spdlog::level_t level_{level::info};
// common implementation for after templated public api has been resolved
template <typename... Args>
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(args...));
#else
fmt::vformat_to(fmt::appender(buf), fmt, fmt::make_format_args(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)
}
};
可以看到:
- 构造函数中可以配置Sink的位置,因为是list,所以可以配置多个Sink
- 通过set_formatter,可以任意配置Formatter
我们主要看log_方法,它接收几个参数:
- source_loc:主要包含文件名、函数名、行号等信息,在Log Macro宏处会自动构造这个对象
- level_enum:日志级别,日志级别低于配置的级别时,不会输出
- fmt & args:实际的日志内容。
log_中首先根据日志级别判断是否要写日志,详见:
bool should_log(level::level_enum msg_level) const {
return msg_level >= level_.load(std::memory_order_relaxed);
}
而 fmt::vformat_to对应架构图中的resolve,这个fmt也进入了C++20标准,感兴趣的可以了解下。还剩 Formatter 和 Sink 的流程没有介绍,继续看log_it_方法:
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);
}
}
那Formatter去哪里了?其实在sink里,拿个 basic-file-sink 举例:
template <typename Mutex>
SPDLOG_INLINE void basic_file_sink<Mutex>::sink_it_(const details::log_msg &msg) {
memory_buf_t formatted;
base_sink<Mutex>::formatter_->format(msg, formatted);
file_helper_.write(formatted);
}
Formatter代码在哪里? #
看代码:
class formatter {
public:
virtual ~formatter() = default;
virtual void format(const details::log_msg &msg, memory_buf_t &dest) = 0;
virtual std::unique_ptr<formatter> clone() const = 0;
};
spdlog提供了标准的formatter,我们也可以自定义formatter,然后配置给logger。详见:formatter.h
Sink代码在哪里? #
namespace sinks {
class SPDLOG_API sink {
public:
virtual ~sink() = default;
virtual void log(const details::log_msg &msg) = 0;
virtual void flush() = 0;
virtual void set_pattern(const std::string &pattern) = 0;
virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) = 0;
void set_level(level::level_enum log_level);
level::level_enum level() const;
bool should_log(level::level_enum msg_level) const;
protected:
// sink log level - default is all
level_t level_{level::trace};
};
从图中看,spdlog提供了好多个sink,我们可以选择性使用。至此,整个spdlog的架构已经清晰,在源码中也已经定位到具体模块,细节就不多介绍,细节大家自己看就可以。