spdlog源码学习(上)
写在前面
我使用的spdlog版本是1.10.0,spdlog的最新版可以在https://github.com/gabime/spdlog找到。本文相关的代码和笔记可以在我的github仓库: https://github.com/KuiH/read-spdlog1.10.0找到。仓库中的代码有我自己加的注释。
由于我本人也是出于学习的目的想要读读这份源码,并且我还很菜,所以下面的内容难免有出错的地方,欢迎大家在评论区指出错误,或者私信我也可以!
简单的目录
一、开端
1.1. 总览
//选自 https://www.cnblogs.com/shuqin/p/12214439.html
spdlog
├─example 用法代码
├─include 实现目录
│ └─spdlog
│ ├─details 功能函数目录
│ ├─fmt {fmt} 库目录
│ ├─sinks 落地文件格式实现
│ └─*.h 异步模式,日志库接口等实现
├─src .cpp 文件,组成编译模块生成静态库使用
├─test 测试代码
别人画的类图(spdlog源码分析-整体框架_spdlog架构_注定会的博客-CSDN博客):
![img](https://i-blog.csdnimg.cn/blog_migrate/e5d6107f98d732a248b39edeebbc57e1.png)
1.2. level
日志的级别在include\spdlog\common.h
中,由枚举level_enum
给出。由低到高为trace debug info warn err critical off
。
相关主要函数有to_string_view
和from_str
,功能分别为根据枚举获取字符串和根据字符串获取枚举值。
这些内容都在namespace level
中
二、example
这节简单看一下example。
几点发现:
- 支持格式化、多线程、异步、多输出目标、计时、自定义类型格式化、自定义异常信息处理等功能
- spdlog::[info/warn/…]使用的是默认的logger作为输出
- spdlog::xxx_xxx_[mt/st]自己新建logger时用的api。
下阶段目标是看懂默认的输出(以info为例)。
输出一条日志的过程大致为:spdlog::info -> details::registry中的默认logger.info -> 构造details::log_msg -> logger::log_it_ -> 该logger的所有sink.log
看起来要先弄清 details::log_msg和sink。
三、details::log_msg
3.1. source_loc结构体
位于include\spdlog\common.h
。记录了打日志的地方所在的文件名、行数、函数名
struct source_loc
{
SPDLOG_CONSTEXPR source_loc() = default;
SPDLOG_CONSTEXPR source_loc(const char *filename_in, int line_in, const char *funcname_in)
: filename{filename_in}
, line{line_in}
, funcname{funcname_in}
{}
SPDLOG_CONSTEXPR bool empty() const SPDLOG_NOEXCEPT
{
return line == 0;
}
const char *filename{nullptr};
int line{0};
const char *funcname{nullptr};
};
SPDLOG_CONSTEXPR宏在我的环境下被扩展成了constexpr
。
constexpr构造函数,把这个类变为一个"字面值常量类"https://blog.csdn.net/craftsman1970/article/details/80244873。constexpr构造函数的函数体一般为空,使用初始化列表或者其他的constexpr构造函数初始化所有数据成员。
constexpr修饰其它函数,将运算尽量放在编译阶段。函数只能有一个return语句
= default 启用默认的构造函数
SPDLOG_NOEXCEPT宏在我的环境下被扩展成了noexcept
。该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序
3.2. log_msg
构造函数中提供了多种构造。成员如下,包括logger名、消息的级别、线程id、位置等
string_view_t logger_name;
level::level_enum level{level::off};
log_clock::time_point time; //using log_clock = std::chrono::system_clock; time_point表示一个时间点
size_t thread_id{0};
// wrapping the formatted text with color (updated by pattern_formatter).
mutable size_t color_range_start{0};
mutable size_t color_range_end{0};
source_loc source;
string_view_t payload; //存储的是msg
构造时如果不指定时间,默认为当前时间。
四、sinks
所有的sink都在include\spdlog\sinks
目录下。
4.1. sink
sink.h
中的sink
类为纯虚的抽象类。其中待实现的接口如下:
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;
should_log
实现如下:
SPDLOG_INLINE bool spdlog::sinks::sink::should_log(spdlog::level::level_enum msg_level) const
{
// std::memory_order_relaxed: 仅仅保证load()和store()是原子操作,除此之外,不提供任何跨线程的同步
return msg_level >= level_.load(std::memory_order_relaxed);
}
当传入的level大于等于sink的level时则通过。
有成员:
level_t level_{level::trace}; // using level_t = std::atomic<int>;
//可见sink的默认级别为trace(级别0)
4.2. base_sink
template<typename Mutex>
class SPDLOG_API base_sink : public sink
{
public:
base_sink();
// explicit禁止构造函数被用于隐式转换
explicit base_sink(std::unique_ptr<spdlog::formatter> formatter);
~base_sink() override = default;
base_sink(const base_sink &) = delete;
base_sink(base_sink &&) = delete;
base_sink &operator=(const base_sink &) = delete;
base_sink &operator=(base_sink &&) = delete;
// 下面这几个是继承来的纯虚函数。final阻止了这些虚函数被子类重载
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);
};
继承来的4个虚函数调用protected里面的4个函数,调用前都用std::lock_guard<Mutex> lock(mutex_)
加了锁。sink_it_
函数负责将msg格式化成字符串buffer以及输出;flush_
将缓冲区的内容写入输出。
其它的xxx_sink要么继承sink,要么继承base_sink。
4.3. basic_file_sink
以basic_file_sink为例子1,该类继承自base_sink<Mutex>
。
template<typename Mutex>
class basic_file_sink final : public base_sink<Mutex>
{
public:
explicit basic_file_sink(const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {});
const filename_t &filename() const;
protected:
void sink_it_(const details::log_msg &msg) override;
void flush_() override;
private:
details::file_helper file_helper_;
};
上面sink_it_
函数负责格式化msg(调用base_sink<Mutex>
中formatter_
成员的接口format
)以及将格式化后的结果输出到文件。
file_event_handlers
如下。用于定义打开和关闭文件所调用的操作。
struct file_event_handlers
{
std::function<void(const filename_t &filename)> before_open;
std::function<void(const filename_t &filename, std::FILE *file_stream)> after_open;
std::function<void(const filename_t &filename, std::FILE *file_stream)> before_close;
std::function<void(const filename_t &filename)> after_close;
file_event_handlers()
: before_open{nullptr}
, after_open{nullptr}
, before_close{nullptr}
, after_close{nullptr}
{}
};
details::file_helper
用来完成一些文件打开、关闭、写入、flush等操作。同时联动file_event_handlers
,如下。除了默认构造函数,还提供了个接受file_event_handlers
的构造函数。析构调用close()
.
SPDLOG_INLINE void file_helper::close()
{
if (fd_ != nullptr)
{
if (event_handlers_.before_close)
{
event_handlers_.before_close(filename_, fd_);
}
std::fclose(fd_);
fd_ = nullptr;
if (event_handlers_.after_close)
{
event_handlers_.after_close(filename_);
}
}
}
对于单线程和多线程的basic_file_sink
,定义了2个类型:
using basic_file_sink_mt = basic_file_sink<std::mutex>;
using basic_file_sink_st = basic_file_sink<details::null_mutex>;
其中,details::null_mutex
如下,就是把std::lock_guard<Mutex> lock(mutex_)
中**需要的操作都变为“空”**即可,有点巧妙。在文件include\spdlog\details\null_mutex.h
中。
struct null_mutex
{
void lock() const {}
void unlock() const {}
bool try_lock() const
{
return true;
}
};
basic_file_sink.h
中暴露出了下面2个函数,分别构造默认的多/单线程logger。
template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> basic_logger_mt(
const std::string &logger_name, const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {})
template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> basic_logger_st(
const std::string &logger_name, const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {})
具体是调用synchronous_factory的create
函数实现。函数信息如下。创建logger同时会向注册中心(details::registry)
注册该logger。
template<typename Sink, typename... SinkArgs>
static std::shared_ptr<spdlog::logger> create(std::string logger_name, SinkArgs &&... args)
4.4. rotating_file_sink
以rotating_file_sink为例子2。Rotating file即当一个日志文件达到指定大小后,就创建一个新的日志文件,以防止一个日志文件过大。
template<typename Mutex>
class rotating_file_sink final : public base_sink<Mutex>
{
public:
rotating_file_sink(filename_t base_filename, std::size_t max_size, std::size_t max_files, bool rotate_on_open = false,
const file_event_handlers &event_handlers = {});
static filename_t calc_filename(const filename_t &filename, std::size_t index);
filename_t filename();
protected:
void sink_it_(const details::log_msg &msg) override;
void flush_() override;
private:
// Rotate files:
// log.txt -> log.1.txt
// log.1.txt -> log.2.txt
// log.2.txt -> log.3.txt
// log.3.txt -> delete
void rotate_();
// delete the target if exists, and rename the src file to target
// return true on success, false otherwise.
bool rename_file_(const filename_t &src_filename, const filename_t &target_filename);
filename_t base_filename_;
std::size_t max_size_;
std::size_t max_files_;
std::size_t current_size_;
details::file_helper file_helper_;
};
可以看到,总体上和basic_file_sink差不多,都有sink_it_
、flush_
函数以及file_helper_
。不同的是多了些进行rotating需要的函数和属性。
这部分函数的注释本身写得就比较清楚,这里我们重点关注rotate_
。通过该函数的实现,结合本身提供的注释可以发现,rotating的实现是通过重命名的方式将已经存在的文件编号+1,最早创建的文件(编号最大的)删除。整个过程中file_helper_
的内容始终是关于index=0的文件(即base_filename
)。rotate_
之后index=0的文件会被清空(原先index=0的文件变为index=1)。
sink_it_
主要的功能仍然是将msg进行格式化并写入文件。与base_file_sink
不同的是,如果写入后的文件大小超过了预定值,则会先rotate_
再写入。
与base_file_sink
一样,rotating_file_sink
也暴露出了构造默认的多/单线程logger的函数,长得和base_file_sink
的差不多,这里就不分析了。
从上面的例子可以看出,各种sink的核心函数还是sink_it_
和flush_
。对于不同的sink,还会有辅助该sink的一些函数。目录include\spdlog\sinks
下有其它的sink,感兴趣可以看看。
五、普通logger
从第二节example中分析的大致过程来看,sink看完了就可以看看logger。看起来这个类就很重要。相关内容在include\spdlog\logger.h
和include\spdlog\logger-inl.h
。
5.1. logger
属性如下:
protected:
std::string name_;
std::vector<sink_ptr> sinks_;
spdlog::level_t level_{level::info};
spdlog::level_t flush_level_{level::off};
err_handler custom_err_handler_{nullptr};
details::backtracer tracer_;
name_
、sinks_
、level_
、flush_level_
比较好理解,表示logger的名字、含有的sink、logger级别、进行flush操作的级别。默认logger级别为info。custom_err_handler_
根据名字推测是用户自定义的错误处理方法之类的。tracer_
的作用可以根据details::backtracer
类提供的注释和example.cpp
中的代码得到,作用是把若干条log_msg存到一个循环buffer中(具体是循环队列)。需要时再将buffer里面的log_msg输出。
下面看函数。
映入眼帘的就是一堆构造函数和一堆接受各种各样参数的log
函数。
构造函数这就不说了,我们重点放在log
函数中。
这些log
函数互相调用,形成调用链,参数少的添加默认值后调用参数多的。根据传入log
的参数,可以把调用链分成2大类:
-
参数中不含有格式化字符串(即"aa{}aa"这种)且要输出的内容只含有string_view_t类型(我的理解是字符串字面值):这一类的
log
函数最终会调用下面两个函数://如果有time_point传入,则调用第一个。 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)
在这两个函数中会构造
details::log_msg
并调用log_it_
进行输出。 -
其它。即含有格式化字符串或要输出的内容是其它类型(如int):这类函数最终会调用下面的
log_
:template<typename... Args> void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&...args)
在
log_
中会格式化字符串、构造details::log_msg
并调用log_it_
进行输出。
因此,所有的log
函数最终都会构造details::log_msg
并调用log_it_
。下面我们来看看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);
}
if (traceback_enabled)
{
tracer_.push_back(log_msg);
}
}
可以看出,如果level符合输出要求,则调用logger的sink_it_
进行输出。如果使用tracer的话,还会将该log_msg缓存进tracer。
因此再看看sink_it_
:
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_();
}
}
可以看出,核心内容为检查每个sink的level是否达到输出要求,并让符合要求的sink调用它的log
函数进行输出。sink的相关函数在第四节进行过分析。
logger和sink都有should_log
函数。从should_log
的代码可以看出,一条msg要想被输出,其level必需大于等于logger的level以及sink的level。
至此,logger的核心部分就介绍完了!
5.2. backtracer
讲完了核心部分,下面就看看logger中的details::backtracer
类型的成员tracer_
吧。相关文件在include\spdlog\details\backtracer.h
以及include\spdlog\details\backtracer-inl.h
。
根据前面提到的,backtracer
是用来存储若干条log_msg,并在需要时一次性将存储的log_msg输出。知道这一点后再度源码应该会更容易理解。类的属性如下:
mutable std::mutex mutex_;
std::atomic<bool> enabled_{false};
circular_q<log_msg_buffer> messages_;
其中,mutex_
用于多线程互斥;enabled_
判断该tracer是否开启;messages_
为tracer中存储的所有信息。
messages_
的类型中,circular_q
为循环队列;log_msg_buffer
为tracer存储的log_msg信息,包括logger_name和payload。
为什么不直接存log_msg类,而要弄一个log_msg_buffer
呢?根据log_msg_buffer
的注释和实现,我个人推测是因为log_msg中的logger_name和payload是string_view_t
类型,是在栈区,而这里需要把它们放到堆区,所以搞了个log_msg_buffer
。
个人认为,比较核心的成员函数为enable(size_t size)
和disable
。
-
enable(size_t size)
将属性
enabled_
改为true,并设置messages_
最多存储size
条数据。 -
disable
将属性
enabled_
改为false
5.3. err_handler
接着看看logger的最后一个成员属性:custom_err_handler_
,类型为err_handler
,定义在include\spdlog\common.h
:
using err_handler = std::function<void(const std::string &err_msg)>;
其实custom_err_handler_
就是一个函数,用于对错误消息进行处理,在err_handler_
成员函数中被调用:
SPDLOG_INLINE void logger::err_handler_(const std::string &msg)
{
if (custom_err_handler_)
{
custom_err_handler_(msg);
}
else
{
//默认err_handler
}
}
而err_handler_
成员函数在对logger进行try-catch时被调用,是由宏SPDLOG_LOGGER_CATCH(location)
负责。
至此,我们便介绍完了logger的部分。
六、registry
还记得我们的目标吗:看懂默认的输出(以info为例)。这是最后一步啦!
6.1. registry
registry
是用来管理所有的logger的。首先浏览一下成员属性和成员函数。registry
的成员属性较多,因此等用到的时候再介绍。注意到它的构造函数是私有的,并且有个static registry &instance();
函数,可以判断这玩意是个单例。
看看默认构造函数:
SPDLOG_INLINE registry::registry()
: formatter_(new pattern_formatter())
{
#ifndef SPDLOG_DISABLE_DEFAULT_LOGGER
// create default logger (ansicolor_stdout_sink_mt or wincolor_stdout_sink_mt in windows).
# ifdef _WIN32
auto color_sink = std::make_shared<sinks::wincolor_stdout_sink_mt>();
# else
auto color_sink = std::make_shared<sinks::ansicolor_stdout_sink_mt>();
# endif
const char *default_logger_name = "";
default_logger_ = std::make_shared<spdlog::logger>(default_logger_name, std::move(color_sink));
loggers_[default_logger_name] = default_logger_;
#endif // SPDLOG_DISABLE_DEFAULT_LOGGER
}
默认的sink为带颜色的sink,根据平台的不同,在wincolor_stdout_sink_mt和ansicolor_stdout_sink_mt中选择。默认的logger名字为空字符串,并将该logger保存到成员default_logger_
,且由成员loggers_
记录。loggers_
是一个哈希表,保存由logger_name到logger的映射。
继续看成员函数。
void register_logger(std::shared_ptr<logger> new_logger)
注册一个新logger,也即添加一个logger。如果logger名字已存在则抛出异常。
void initialize_logger(std::shared_ptr<logger> new_logger)
根据registry
自身的成员变量设置传入logger的属性,如formatter、error_handler、level等。如果开启自动注册,则会将new_logger添加进loggers_
当中。
void set_default_logger(std::shared_ptr<logger> new_default_logger)
顾名思义,如果不想要它默认的logger的话,就手动设置默认logger。
logger *get_default_raw()
default_logger_
成员属性是一个智能指针。这个函数的作用是获取其原始指针(raw ptr)。这个函数是用于spdlog的默认api,如spdlog::info。它可以让默认api更快。但是**默认api不能与set_default_logger
成员函数在不同线程同时使用,**否则logger就会串了。
void flush_every(std::chrono::seconds interval)
每隔一定周期就调用flush_all
成员函数(由于flush_all
较简单,这里就不介绍),让所有logger都flush一遍。最后再给成员periodic_flusher_
赋值。periodic_flusher_
是一个periodic_worker
类的实例。periodic_worker
根据名字看,是负责以一定周期实行某个任务,我们在后面会介绍它。
void apply_all(const std::function<void(const std::shared_ptr<logger>)> &fun)
将传入的fun
运用在所有logger上。
void set_levels(log_levels levels, level::level_enum *global_level)
log_levels
定义为using log_levels = std::unordered_map<std::string, level::level_enum>
,是logger_name到其level的映射。该函数给registry中的所有logger设置level,并更新成员属性log_levels_
。如果logger_name出现在log_levels
中,则以log_levels
中对应的level为准;若没出现且global_level
不为nullptr,则以global_level
为准;若都不满足,则该logger的level不变。
如果global_level
不为空,还会更新registry的成员global_log_level_
。
至此,registry
中个人认为比较重要的成员函数就介绍完了。其余的都是些成员属性的getter和setter,都是先加锁后操作,代码比较简单。值得注意的是,对于线程池tp_
,加锁是用的是std::recursive_mutex
,其余都是std::mutex
。std::recursive_mutex
允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
6.2. periodic_worker
主要部分完成后,来看看前面提到的periodic_worker
类吧。正如前面提到的,periodic_worker
负责以一定周期实行某个任务。看看它是怎么实现的。
成员变量:
bool active_; // 该worker是否工作
std::thread worker_thread_; // 工作线程
std::mutex mutex_; // 互斥量
std::condition_variable cv_; // 同步量
在成员函数方面,就只有一个构造和一个析构。
- 构造函数**
periodic_worker(const std::function<void()> &callback_fun, std::chrono::seconds interval)
**
callback_fun
为要执行的内容,interval
为执行的周期。函数实现如下:
SPDLOG_INLINE periodic_worker::periodic_worker(const std::function<void()> &callback_fun, std::chrono::seconds interval)
{
active_ = (interval > std::chrono::seconds::zero());
if (!active_)
{
return;
}
//线程在构建完毕后就会开始运行
worker_thread_ = std::thread([this, callback_fun, interval]() {
for (;;)
{
std::unique_lock<std::mutex> lock(this->mutex_);
//wait_for(锁,时间周期,pred谓词判断)
if (this->cv_.wait_for(lock, interval, [this] { return !this->active_; }))
{
return; // active_ == false, so exit this thread
}
callback_fun();
}
});
}
直接看重点的worker_thread_
部分。该部分使用一个lambda表达式创建一个新的后台线程。该新线程会在一个死循环中等待interval的时间,如果期间active_变为false,则退出循环,结束线程;否则,调用回调函数 callback_fun执行任务。至于active_如何变为false,就要看析构函数了。涉及到cv
时,锁要用unique_lock
,不能用lock_guard
,以便让解锁由cv
管理,而不是自动管理。
- 析构函数**
~periodic_worker()
**
// stop the worker thread and join it
SPDLOG_INLINE periodic_worker::~periodic_worker()
{
if (worker_thread_.joinable())
{
{
std::lock_guard<std::mutex> lock(mutex_);
active_ = false;
}
cv_.notify_one(); //这个是主线程调用的
worker_thread_.join();
}
}
先将active_
改为false,而后通过条件变量 cv_
发送通知,唤醒正在等待的worker_thread_
,此时由于active_
为false,则构造函数中cv_.wait_for
会返回true,从而使worker_thread_
返回。
调用worker_thread_.join()
是为了等待worker_thread_
结束。这样,当periodic_worker
对象被销毁时,会停止worker_thread_
,并等待其结束,确保资源被正确释放。
6.3. 小总结
至此,registry
中我想讲的部分就结束啦!现在,我们已经有能力描述spdlog::info
到底干了什么:
- 调用
registry::instance()
,初始化registry
以及default_logger; - 获取default_logger的原始指针,调用其
info
函数; - 进入
logger
类内部,调用log
函数,log
函数会经过调用链,最终构造details::log_msg
并调用log_it_
函数; - 在
log_it_
中调用sink_it_
进行输出。如果使用tracer的话,还会将log_msg缓存进tracer; sink_it_
函数遍历logger的所有sink,调用sink的log
函数;- sink的
log
函数是在base_sink
中实现,该函数加锁并调用sink的sink_it_
函数; sink_it_
函数会因为sink的不同而不同,但主要功能是格式化msg以及输出格式化后的结果。
其中,一条msg要想被输出,其level必需大于等于logger的level以及sink的level。
以上便是spdlog::info
的流程,有没有一种苦尽甘来,收获颇丰的感觉哈哈哈哈!
下面是我在第一节类图的基础上修改后的类图。注意:出于好看和偷懒的考虑,类图中并没有展示出全部的成员函数,参数的类型也被忽略。
下半部分博客链接:
https://blog.csdn.net/weixin_51063895/article/details/132392927