spdlog 源码解析
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
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)
}
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 // 虚基类:用于为外部调用提供统一接口 -> basic_sink // 模板类:用于封装线程安全的接口 // 具体sink实现 -> basic_file_sink -> hourly_file_sink -> daily_file_sink -> rotating_file_sink -> stdout_sink_base // 模板类:用于封装线程安全的接口(主要用于控制台的输出) -> stdout_sink -> stderr_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类
先来看如何设置日志格式:
关键步骤为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
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); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现