spdlog是开源、高性能、跨平台,支持header-only/compiled的C++日志库。
本文主要目的是对spdlog v1.13.0的源码进行分析(编译运行环境为6.5.0-18-generic #18~22.04.1-Ubuntu),以探讨spdlog如何构建高性能、可扩展的日志框架的。
github链接
gabime/spdlog: Fast C++ logging library. (github.com)
应用示例
spdlog/README.md at v1.x · gabime/spdlog (github.com)
spdlog代码特点
spdlog主要基于C++11开发(若编译环境支持C++20,则将使用std::fmt取代第三方fmt库)
spdlog中大量使用移动语义、完美转发以减少对象拷贝,又利用内联、模板等技术尽量减少了抽象的代价
同时广泛使用了智能指针等降低了内存管理的复杂性,通过spdlog可以深入的了解C++11的优雅实现
spdlog架构
-
logger/async_logger
日志处理的入口,负责格式化日志信息、日志信息的整理合并(如日志级别、文件名、函数名、文件行号等),最终封装至log_msg对象中,再将log_msg对象投递给下游处理。
logger与aync_logger区别在于:
logger是同步处理,会由调用日志记录的线程直接将封装后的log_msg对象投递给下游的sink
aync_logger则是异步处理,调用日志记录的线程仅负责将封装后的log_msg对象放入线程安全队列,后续由线程池从线程安全队列中不断处理队列中的日志对象
-
sink
负责接收log_msg对象,并通过formatter将对象中记录的信息转换为字符串,最终将字符串输出到目标位置(控制台、日志文件等)
-
formatter
负责将log_msg对象中的信息转换成字符串
-
registry
负责管理所有的logger(创建、销毁、获取等),并且通过registry还可对所有的logger做全局设置
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()) }
标签:log,源码,spdlog,msg,日志,解析,logger,sink From: https://www.cnblogs.com/pond-flower/p/18062789