系列目录
spdlog日志库–基础介绍
spdlog日志库–源码解析
spdlog日志库–输出格式(fmt 库集成)
spdlog日志库–使用案例
目录
本文主要目的是对spdlog v1.13.0的源码进行分析,以便后续更好使用spdlog日志库。参考:链接
logger
以调用logger.warn为例,其调用栈基本可分为如下两类:
无需字符串格式化时
logger.warn("hello world!");
调用栈:
template <typename T>
void warn(const T &msg)
--> void log(level::level_enum lvl, string_view_t msg); // 增加日志等级作为参数传入
--> void log(source_loc loc, level::level_enum lvl, string_view_t msg); // 增加文件名、函数名、文件行号作为参数传入
--> void log(source_loc loc, level::level_enum lvl, string_view_t msg); // 将日志信息封装至details::log_msg结构体中
--> void logger::log_it_(const spdlog::details::log_msg &log_msg, bool log_enabled, bool traceback_enabled);
--> void logger::sink_it_(const details::log_msg &msg); // 将封装后的details::log_msg结构体投递给下游的sink
需要字符串格式化时
logger.warn("hello world! {}", "pond-flower");
调用栈:
template <typename... Args>
void warn(format_string_t<Args...> fmt, Args &&...args);
--> template <typename... Args>
void log(level::level_enum lvl, format_string_t<Args...> fmt, Args &&...args); // 增加日志等级作为参数传入
--> template <typename... Args>
void log(source_loc loc, level::level_enum lvl, format_string_t<Args...> fmt, Args &&...args); // 增加文件名、函数名、文件行号作为参数传入
--> template <typename... Args>
void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&...args); // 字符串格式化后将日志信息封装至details::log_msg结构体中
--> void logger::log_it_(const spdlog::details::log_msg &log_msg, bool log_enabled, bool traceback_enabled);
--> void logger::sink_it_(const details::log_msg &msg); // 将封装后的details::log_msg结构体投递给下游的sink
结合上述调用栈可看出两种日志记录形式殊途同归,最终将日志信息封装至details::log_msg后投递给下游的sink进行处理。
其中对日志信息最重要的处理行为是将日志信息封装至details::log_msg以及将封装后的detail::log_msg结构体投递给下游的sink。
其源码如下:
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(); // 判断是否需要traceback
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...)); // 字符串格式化处理(使用std::fmt)
#else
fmt::vformat_to(fmt::appender(buf), fmt, fmt::make_format_args(args...)); // 字符串格式化处理(使用第三方fmt库)
#endif
details::log_msg log_msg(loc, name_, lvl, string_view_t(buf.data(), buf.size())); // 封装日志信息至log_msg对象中
log_it_(log_msg, log_enabled, traceback_enabled); // 将log_msg对象投递给sink进行处理
}
SPDLOG_LOGGER_CATCH(loc)
}
SPDLOG_INLINE void logger::sink_it_(const details::log_msg &msg) {
// 遍历所有sink,将msg交由各个sink进行处理
// 单个logger对象可对应多个sink——即单个输入端允许对应多个输出端
// 以此实现了同份日志内容可输出至日志文件、控制台等
for (auto &sink : sinks_) {
if (sink->should_log(msg.level)) {
SPDLOG_TRY { sink->log(msg); }
SPDLOG_LOGGER_CATCH(msg.source)
}
}
// 根据日志级别是否超出flush_level_判断是否需要立即对sink进行flush操作
if (should_flush_(msg)) {
flush_();
}
}
总结
logger的实现中大量使用了可变参数模板与模板实例化技术,逐步将日志所需要的信息逐层传递,同时通过模板内联避免了函数逐层传递带来的性能损耗,代码简洁易懂、扩展性强,极具参考意义。
async-logger
async-logger
继承自logger
,通过重写父类的sink_it_
、flush_
以实现由线程池执行sink->log(msg)、sink->flush(),实现了异步日志打印。
因此async-logger
相较于logger
新增了两个成员变量。
std::weak_ptr<details::thread_pool> thread_pool_; // 线程池对象
async_overflow_policy overflow_policy_; // 日志队列满时处理策略:阻塞、丢弃新日志、丢弃旧日志
重写后的sink_it_源码如下
> SPDLOG_INLINE void spdlog::async_logger::sink_it_(const
> details::log_msg &msg){
> SPDLOG_TRY{if (auto pool_ptr = thread_pool_.lock()){
> pool_ptr->post_log(shared_from_this(), msg, overflow_policy_); // 将日志消息投递到线程池的消息队列中 } else {
> throw_spdlog_ex("async log: thread pool doesn't exist anymore"); } } SPDLOG_LOGGER_CATCH(msg.source) }
重写后的flush_源码如下:
SPDLOG_INLINE void spdlog::async_logger::flush_()
{
SPDLOG_TRY{if (auto pool_ptr = thread_pool_.lock())
{
pool_ptr->post_flush(shared_from_this(), overflow_policy_); // 将flush请求投递到线程池的消息队列中
}
else {
throw_spdlog_ex("async flush: thread pool doesn't exist anymore");
}
SPDLOG_LOGGER_CATCH(source_loc());
}
sink
sink模块相关代码位于sinks目录下,spdlog提供了多种sink实现,以此支持向控制台、文件、数据库、消息队列等目的端输出日志。
其类继承模式基本如下:
sink // 虚基类:用于为外部调用提供统一接口
-> basic_sink // 模板类:用于封装线程安全的接口
// 具体sink实现
-> basic_file_sink
-> hourly_file_sink
-> daily_file_sink
-> rotating_file_sink
-> stdout_sink_base // 模板类:用于封装线程安全的接口(主要用于控制台的输出)
-> stdout_sink
-> stderr_sink
sink类
是所有不同类型sink的虚基类,用于提供统一的接口封装,其定义如下:
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}; // 原子变量以保证日志等级获取、判断的线程安全性
};
从上述定义上可看出,sink其实并没有在内部进行加锁等操作以在基类中保证log与flush调用的线程安全,实际上线程安全是在base_sink提供的。
base_sink类
base_sink继承自sink,是模板类,支持自定义锁类型,相较于sink而言主要在各处操作前通过自定义锁保证线程安全。
主要实现如下:
template <typename Mutex>
class SPDLOG_API base_sink : public sink {
public:
void log(const details::log_msg &msg) final;
void flush() final;
void set_pattern(const std::string &pattern) final;
void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) final;
protected:
// sink formatter
std::unique_ptr<spdlog::formatter> formatter_;
Mutex mutex_; // 用于保证线程安全(根据模板参数决定锁类型、甚至采用无锁实现)
virtual void sink_it_(const details::log_msg &msg) = 0;
virtual void flush_() = 0;
virtual void set_pattern_(const std::string &pattern);
virtual void set_formatter_(std::unique_ptr<spdlog::formatter> sink_formatter);
};
template <typename Mutex>
void SPDLOG_INLINE spdlog::sinks::base_sink<Mutex>::log(const details::log_msg &msg) {
std::lock_guard<Mutex> lock(mutex_);
sink_it_(msg);
}
template <typename Mutex>
void SPDLOG_INLINE spdlog::sinks::base_sink<Mutex>::flush() {
std::lock_guard<Mutex> lock(mutex_);
flush_();
}
由于base_sink为模板类实现,可自定义锁类型,如使用互斥锁、读写锁等,并且可以优雅的使base_sink同时支持无锁版本与有锁版本,以basic_file_sink为例,其具体实现方式如下:
struct null_mutex {
void lock() const {}
void unlock() const {}
};
using basic_file_sink_mt = basic_file_sink<std::mutex>; // 有锁版本,适用于多线程场景
using basic_file_sink_st = basic_file_sink<details::null_mutex>; // 无锁版本,适用于单线程场景
basic_file_sink类
basic_file_sink继承自basic_sink,是将日志输出至单个目标文件的具体sink子类实现,其主要实现了父类的sink_it与flush_接口,其具体实现如下:
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); // 将log_msg格式化为字符串
file_helper_.write(formatted);
}
template <typename Mutex>
SPDLOG_INLINE void basic_file_sink<Mutex>::flush_() {
file_helper_.flush();
}
以文件作为输出端的sink类还有hourly_file_sink、daily_file_sink、rotating_file_sink等,实现上基本与basic_file_sink类似,仅是在其之上提供了文件转储等能力。
stdout_sink_base类
stdout_sink_base
类似于basic_sink
,其与basic_sink
的主要区别在于其成员变量mutex_类型不同,其中stdout_sink_base
的成员变量mutex_为引用类型,而basic_sink
的成员变量mutex_为非引用类型。
由此可看出stdout_sink_base主要应用于多个sink对象共享同个互斥量的场景,而控制台的输出则是上述场景的典型示例。
stdout_sink_base同样重写了父类sink的log与flush接口。
具体实现如下:
template <typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::log(const details::log_msg &msg) {
#ifdef _WIN32
if (handle_ == INVALID_HANDLE_VALUE) {
return;
}
std::lock_guard<mutex_t> lock(mutex_);
memory_buf_t formatted;
formatter_->format(msg, formatted);
auto size = static_cast<DWORD>(formatted.size());
DWORD bytes_written = 0;
bool ok = ::WriteFile(handle_, formatted.data(), size, &bytes_written, nullptr) != 0;
if (!ok) {
throw_spdlog_ex("stdout_sink_base: WriteFile() failed. GetLastError(): " +
std::to_string(::GetLastError()));
}
#else
std::lock_guard<mutex_t> lock(mutex_);
memory_buf_t formatted;
formatter_->format(msg, formatted);
::fwrite(formatted.data(), sizeof(char), formatted.size(), file_);
#endif // WIN32
::fflush(file_); // flush every line to terminal
}
template <typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::flush() {
std::lock_guard<mutex_t> lock(mutex_);
fflush(file_);
}
formatter
在介绍sink时其实已经有使用formatter的示例:
memory_buf_t formatted;
formatter_->format(msg, formatted);
formatter将log_msg结构体以预定义的格式转化为字符串存放于formatted中。
formatter类
类似于sink模块的sink类,同样为虚基类,用于为外部提供统一的接口,其定义如下:
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;
};
上述formatter_实际类型为pattern_formatter对象的智能指针,但在介绍pattern_formatter前,先来了解下flag_formatter。
flag_formatter类
先来看如何设置日志格式:
spdlog::set_pattern("[%H点%M分]");
调用栈:
void logger::set_pattern(std::string pattern, pattern_time_type time_type = pattern_time_type::local); // 构造pattern_formatter对象的智能指针
--> void logger::set_formatter(std::unique_ptr<formatter> f); // 遍历logger中所有的sink,依次调用sink->set_formatter
--> void sink::set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter); //
关键步骤为pattern_formatter对象的初始化,其实现如下:
// 自定义格式
// 自定义格式
SPDLOG_INLINE pattern_formatter::pattern_formatter(std::string pattern,
pattern_time_type time_type,
std::string eol,
custom_flags custom_user_flags)
: pattern_(std::move(pattern)),
eol_(std::move(eol)),
pattern_time_type_(time_type),
need_localtime_(false),
last_log_secs_(0),
custom_handlers_(std::move(custom_user_flags)) {
std::memset(&cached_tm_, 0, sizeof(cached_tm_));
compile_pattern_(pattern_); // 解析pattern_
}
// 默认格式
SPDLOG_INLINE pattern_formatter::pattern_formatter(pattern_time_type time_type, std::string eol)
: pattern_("%+"),
eol_(std::move(eol)),
pattern_time_type_(time_type),
need_localtime_(true),
last_log_secs_(0) {
std::memset(&cached_tm_, 0, sizeof(cached_tm_));
formatters_.push_back(details::make_unique<details::full_formatter>(details::padding_info{}));
}
解析pattern_是通过compile_pattern_实现的,具体实现如下:
SPDLOG_INLINE void pattern_formatter::compile_pattern_(const std::string &pattern) {
auto end = pattern.end();
std::unique_ptr<details::aggregate_formatter> user_chars;
formatters_.clear();
// 遍历pattern
for (auto it = pattern.begin(); it != end; ++it) {
if (*it == '%') {
if (user_chars)
{
formatters_.push_back(std::move(user_chars)); // 将flag_formatter追加入formatters_
}
// 字符串对齐处理相关
auto padding = handle_padspec_(++it, end);
if (it != end) {
if (padding.enabled()) {
handle_flag_<details::scoped_padder>(*it, padding);
} else {
handle_flag_<details::null_scoped_padder>(*it, padding);
}
} else {
break;
}
} else // chars not following the % sign should be displayed as is
{
if (!user_chars) {
user_chars = details::make_unique<details::aggregate_formatter>();
}
user_chars->add_ch(*it); // 将当前字符追加入user_chars之后
}
}
if (user_chars) // append raw chars found so far
{
formatters_.push_back(std::move(user_chars));
}
}
pattern_formatter
pattern_formatter利用预处理过的pattern来完成字符串格式化步骤,具体实现如下:
SPDLOG_INLINE void pattern_formatter::format(const details::log_msg &msg, memory_buf_t &dest) {
if (need_localtime_) { // 是否需要记录当前时间
const auto secs =
std::chrono::duration_cast<std::chrono::seconds>(msg.time.time_since_epoch());
if (secs != last_log_secs_) {
cached_tm_ = get_time_(msg); // 更新缓存时间
last_log_secs_ = secs;
}
}
for (auto &f : formatters_) { // 遍历formatters_(std::vector<std::unique_ptr<details::flag_formatter>>)
f->format(msg, cached_tm_, dest); // 不断向dest中追加信息
}
// write eol
details::fmt_helper::append_string_view(eol_, dest);
}