spdlog日志库源码:sinks系列类
sinks系列类简介
sinks并不是一个类,而是一系列类,以基类-派生类形式组织,一个sink派生类代表了一种输出log消息方式,输出目标可以是普通文件stdout、stderr,或者syslog等等。sink系列类主要负责从logger接收用户log消息,按指定模式(pattern)进行格式化(format),得到一条完整的、格式化后的log消息,然后将其写到目标文件。
sink系列类的实现,全部位于include/spdlog/sinks目录。
特点
- 基类-派生类方式,便于扩展新功能
- 每个具体的sink派生类负责一种具体的输出目标
- 支持日志等级设置
- 支持模式(pattern)设置
- 支持自定义格式(formatter)
sinks继承体系
sinks系列类继承体系如下图所示:
每一个具体的派生类负责一类具体的输出目标。
sink类
类sink是所有sinks系列类的基类,也是一个接口类,提供接口和共有数据,但不负责实例化。
sink类声明
- log() 接收用户log消息并写入目标文件,通常是由logger类传入
- flush() 冲刷用户log消息,将缓存中数据尽快写入目标文件
- set_pattern() 由现有的模式标志,定制输出的log消息格式
- set_formatter() 实现自定义formatter,定制输出的log消息格式
set_pattern和set_formatter类似,都是定制日志格式,区别在于后者支持自定义的模式标志。模式标志(pattern flags)是指格式为%flag,类似于strftime的转换字符(如%a, %e etc.)。
sink声明式:
/// 一个sink对象对应一个输出目标, 即文件, 负责将log消息写到指定目标上,
/// 可能是普通文件, syslog, 终端, 或者socket(tcp/udp), etc.
class SPDLOG_API sink
{
public:
virtual ~sink() = default;
// 接收log消息
virtual void log(const details::log_msg &msg) = 0;
// 冲刷log消息
virtual void flush() = 0;
// 设置模式
virtual void set_pattern(const std::string &pattern) = 0;
// 设置formatter(格式)
virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) = 0;
// 设置log等级阈值
void set_level(level::level_enum log_level);
// 获取log等级阈值
level::level_enum level() const;
// 判断是否应当写log消息,msg_level是log消息的log等级
bool should_log(level::level_enum msg_level) const;
protected:
// sink log level - default is all
level_t level_{level::trace};
};
日志等级阈值
should_log()判断是否应该写log消息
sink有一个比较特殊的变量level_,是指sink的日志等级,相当于一个log等级阈值。只有当log消息本身log等级 >= level_时,才写log到目标。
这也是should_log()所干的事情:
// msg_level 是当前log消息的日志等级
SPDLOG_INLINE bool spdlog::sinks::sink::should_log(spdlog::level::level_enum msg_level) const
{
return msg_level >= level_.load(std::memory_order_relaxed);
}
get/set日志等级阈值
sink提供了level_的get、set方法。注意这里并没有直接对leve_使用"="进行赋值,而是使用了适用于内存布局的方法,此举是为了便于修改和扩展其他原子操作,便于确保原子操作顺序一致性。
SPDLOG_INLINE void spdlog::sinks::sink::set_level(level::level_enum log_level)
{
level_.store(log_level, std::memory_order_relaxed);
}
SPDLOG_INLINE spdlog::level::level_enum spdlog::sinks::sink::level() const
{
return static_cast<spdlog::level::level_enum>(level_.load(std::memory_order_relaxed));
}
sink子类
null_sink类模板
前面分析logger类时,知道,如果用户想用logger对象,就必须提供sink对象。但是如果用户不想做任何写文件操作,例如测试代码框架是否能跑通,该怎么办?
答案是可以使用null_sink,这是是一个空类,所有接口皆为空。
template<typename Mutex>
class null_sink : public base_sink<Mutex>
{
protected:
// 实现空接口
void sink_it_(const details::log_msg &) override {}
void flush_() override {}
};
// null_mutex是空锁, lock/unlock操作并不会真正锁住线程
using null_sink_mt = null_sink<details::null_mutex>;
using null_sink_st = null_sink<details::null_mutex>;
有了具体的null_sink类型(null_sink_mt/null_sink_st),我们可以用工厂方法装配出logger对象。于是,spdlog提供便捷的创建sink为null_sink的logger对象的方式:
// 便捷创建logger对象, 其sink为null_sink_mt
template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> null_logger_mt(const std::string &logger_name)
{
auto null_logger = Factory::template create<sinks::null_sink_mt>(logger_name);
null_logger->set_level(level::off);
return null_logger;
}
// 便捷创建logger对象, 其sink为null_sink_st
template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> null_logger_st(const std::string &logger_name)
{
auto null_logger = Factory::template create<sinks::null_sink_st>(logger_name);
null_logger->set_level(level::off);
return null_logger;
}
base_sink类模板
这是sink最核心的一个子类,是一个抽象类类模板,无法实例化,为其他更多的sink提供公共的标准接口。
base_sink
template<typename Mutex>
class SPDLOG_API base_sink : public sink
{
public:
base_sink();
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;
void log(const details::log_msg &msg) final; // 接收用户log消息
void flush() final; // 冲刷用户log消息(到目标文件)
void set_pattern(const std::string &pattern) final; // 用模式串设置模式, 使用默认的formatter=pattern_formatter
void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) final; // 设置formatter指定格式, 支持自定义formatter
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);
};
可以看到,base_sink在基类sink基础上做了一些额外工作,主要是:
1)添加接受formatter为参数的构造器;
2)删除拷贝构造、移动构造函数;
3)删除拷贝赋值、移动赋值运算符;
4)将方法log、flush、set_pattern、set_formatter声明为final,禁止派生类重写,但又增添了virtual版本的protected方法sink_it_、flush_、set_pattern_、set_formatter_,这实际上是模板方法(设计模式)的应用;
5)提供默认的formatter(即pattern_formatter),或自定义的formatter支持;
6)以模板参数Mutex为锁类型,便于用同一套代码实现有锁、无锁两套方案;
base_sink的public接口是线程安全的,只在public接口加锁,并未在protected方法加锁。例如,base_sink
template<typename Mutex>
void SPDLOG_INLINE spdlog::sinks::base_sink<Mutex>::log(const details::log_msg &msg)
{
std::lock_guard<Mutex> lock(mutex_); // 获得锁,确保base_sink<Mutex>数据成员的线程安全
sink_it_(msg); // sink_it_是纯虚函数,实际工作转发给sink_it_
}
log()把工作转发给了virtual函数sink_it_,实际调用的是子类的实现。例如,其中一个子类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); // 将格式化后的二进制数据写入目标文件
}
注意:除了构造器,有数据访问的public接口都加锁了,而非public接口并未加锁。
basic_file_sink类模板
basic_file_sink
basic_file_sink会根据构造器提供的文件名来创建一个log文件,文件支持截断功能。
TIPS:文件截断是指,打开文件时清空文件旧内容,相当于open(2)的O_TRUNC选项。
/*
* Trivial file sink with single file as target
*/
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:
// 实现了2个base_sink声明的pure virtual函数
void sink_it_(const details::log_msg &msg) override;
void flush_() override;
private:
details::file_helper file_helper_; // 文件操作帮助类, 是一个工具类
};
对于构造器,各参数含义如下:
- filename 类型通过别名filename_t进行包装,本质上一个字符串(std::string),因为在Windows可能需要支持宽字符。
- truncate 指定是否使用文件截断功能,在打开文件时决定,通常以write + append或truncate方式打开。
- event_handlers 通过一个结构体file_event_handlers包装了文件操作前后的事件,用户可以通过这种回调函数机制,指定在对应文件事件发生时要进行的动作。支持4类文件事件:打开文件前(before_open)、打开文件后(after_open)、关闭文件前(before_close)、关闭文件后(after_close)。
file_event_handlers定义:
struct file_event_handlers
{
file_event_handlers()
: before_open(nullptr)
, after_open(nullptr)
, before_close(nullptr)
, after_close(nullptr)
{}
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;
};
basic_file_sink实现了2个基类base_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);
file_helper_.write(formatted);
}
template<typename Mutex>
SPDLOG_INLINE void basic_file_sink<Mutex>::flush_()
{
file_helper_.flush();
}
这2个函数很简单,sink_it_ 是利用基类的formatter_,对log消息msg进行格式化(format),转换为二进制数据存放到memory_buf_t缓存中,然后通过工具函数file_helper_.write写到指定文件中。
flush_
则是直接调用工具函数file_helper_.flush冲刷缓存到文件。
文件工具类file_helper
file_helper封装了一些基本文件操作,专门用于日志文件操作,主要包括:打开文件(open)、关闭文件(close)、重新打开文件(reopen)、写文件(write)、求文件大小(size)、获取文件名(filename)、切分文件扩展名(split_by_extension)。
在此基础上,file_helper还提供了一些额外功能,用于确保程序健壮性,或更细粒度控制,例如重试open文件、文件操作事件回调。
// Helper class for file sinks.
// When failing to open a file, retry several times(5) with a delay interval(10 ms).
// Throw spdlog_ex exception on errors.
class SPDLOG_API file_helper
{
public:
file_helper() = default; // 当用户只是想要简单log文件操作时,使用该构造器
explicit file_helper(const file_event_handlers &event_handlers); // 当用户想进行文件操作事件回调时,使用该构造器
// 文件操作涉及底层OS文件资源,因此禁用copy
file_helper(const file_helper &) = delete;
file_helper &operator=(const file_helper &) = delete;
~file_helper();
void open(const filename_t &fname, bool truncate = false);
void reopen(bool truncate);
void flush();
void close();
void write(const memory_buf_t &buf);
size_t size() const;
const filename_t &filename() const;
//
// return file path and its extension:
//
// "mylog.txt" => ("mylog", ".txt")
// "mylog" => ("mylog", "")
// "mylog." => ("mylog.", "")
// "/dir1/dir2/mylog.txt" => ("/dir1/dir2/mylog", ".txt")
//
// the starting dot in filenames is ignored (hidden files):
//
// ".mylog" => (".mylog". "")
// "my_folder/.mylog" => ("my_folder/.mylog", "")
// "my_folder/.mylog.txt" => ("my_folder/.mylog", ".txt")
static std::tuple<filename_t, filename_t> split_by_extension(const filename_t &fname);
private:
const int open_tries_ = 5; // 重试open次数
const unsigned int open_interval_ = 10; // 两次open文件的时间间隔
std::FILE *fd_{nullptr}; // 文件指针
filename_t filename_; // 缓存最近一次open的文件名
file_event_handlers event_handlers_;
};
- open打开文件
实现思路:
SPDLOG_FILENAME_T包装字符串,是为了同时兼容普通字符(std::string)和宽字符(std::wstring)。
对比一般的open,file_helper::open多了事件回调、出差重试等功能。
// fname 要打开的文件
// truncate 是否截断文件
SPDLOG_INLINE void file_helper::open(const filename_t &fname, bool truncate)
{
close();
filename_ = fname;
auto *mode = SPDLOG_FILENAME_T("ab");
auto *trunc_mode = SPDLOG_FILENAME_T("wb");
if (event_handlers_.before_open) // open文件前的事件回调
{
event_handlers_.before_open(filename_);
}
for (int tries = 0; tries < open_tries_; ++tries) // 尝试open_tries_次
{
// create containing folder if not exists already.
os::create_dir(os::dir_name(fname));
if (truncate) // 打开、关闭文件时截断文件
{
// Truncate by opening-and-closing a tmp file in "wb" mode, always
// opening the actual log-we-write-to in "ab" mode, since that
// interacts more politely with eternal processes that might
// rotate/truncate the file underneath us.
std::FILE *tmp;
if (os::fopen_s(&tmp, fname, trunc_mode))
{
continue;
}
std::fclose(tmp);
}
if (!os::fopen_s(&fd_, fname, mode)) // 打开文件
{
if (event_handlers_.after_open)
{
event_handlers_.after_open(filename_, fd_);
}
return;
}
details::os::sleep_for_millis(open_interval_); // 每次延时open_interval_ ms
}
// 多次系统调用error, 则抛出异常
throw_spdlog_ex("Failed opening file " + os::filename_to_str(filename_) + " for writing", errno);
}
像os::fopen_s这类系统调用,是为了屏蔽平台差异,spdlog使用os::fopen_s进行了封装,底层还是_fsopen(Windows)、open/fopen等调用。
- reopen重新打开文件
reopen实现很简单,就是判断一下filename_,然后重新调用open
// reopen是open实现的
SPDLOG_INLINE void file_helper::reopen(bool truncate)
{
if (filename_.empty())
{
throw_spdlog_ex("Failed re opening file - was not opened before");
}
this->open(filename_, truncate);
}
已经有了open,为何还要定义reopen?
可能用户第一次打开文件时,可以截断文件;而后续某个时刻,则不允许截断文件。这样用户可能会有重新打开文件的需求。
- flush冲刷文件
flush没有什么特别的,就是用std::fflush实现,不过多了出错抛出异常。
SPDLOG_INLINE void file_helper::flush()
{
if (std::fflush(fd_) != 0) // 系统调用error
{
throw_spdlog_ex("Failed flush to file " + os::filename_to_str(filename_), errno);
}
}
- close关闭文件
close关闭文件也很简单,通过std::close实现,只是多了时间回调。
- write写文件
write接口接收memory_buf_t参数作为待写内容,而std::write需要const char* + size_t参数,因此,需要进行转换。
SPDLOG_INLINE void file_helper::write(const memory_buf_t &buf)
{
size_t msg_size = buf.size();
auto data = buf.data();
if (std::fwrite(data, 1, msg_size, fd_) != msg_size) // 系统调用error
{
throw_spdlog_ex("Failed writing to file " + os::filename_to_str(filename_), errno);
}
}
- size获取文件长度
size通过系统调用获取文件长度,对于类Unix系统是fstat,对于Windows系统是_filelength/_filelength64。
- filename获取文件名
这个更简单,直接返回最近一次open文件的文件名。
- split_by_extension切分扩展名
split_by_extension根据文件(全)名最后一个"."进行切分,当前得确保前面是合法基础文件名(不能以"/"结尾),后面是合法文件扩展名(不含"/")。
// 返回值是tuple, 参数对应基础文件名, 后缀名
// return file path and its extension:
//
// "mylog.txt" => ("mylog", ".txt")
// "mylog" => ("mylog", "")
// "mylog." => ("mylog.", "")
// "/dir1/dir2/mylog.txt" => ("/dir1/dir2/mylog", ".txt")
//
// the starting dot in filenames is ignored (hidden files):
//
// ".mylog" => (".mylog". "")
// "my_folder/.mylog" => ("my_folder/.mylog", "")
// "my_folder/.mylog.txt" => ("my_folder/.mylog", ".txt")
SPDLOG_INLINE std::tuple<filename_t, filename_t> file_helper::split_by_extension(const filename_t &fname)
{
auto ext_index = fname.rfind('.'); // 找最后一个'.', 就是后缀名切分点
// no valid extension found - return whole path and empty string as
// extension
if (ext_index == filename_t::npos || ext_index == 0 || ext_index == fname.size() - 1)
{
return std::make_tuple(fname, filename_t()); // 后缀名为空
}
// treat cases like "/etc/rc.d/somelogfile or "/abc/.hiddenfile"
// folder_seps_filename是指文件名分隔符"\\/"(Windows)或"/"(non-Windows)
auto folder_index = fname.find_last_of(details::os::folder_seps_filename);
if (folder_index != filename_t::npos && folder_index >= ext_index - 1) // 后缀名中包含".", 说明后缀名无效
{
return std::make_tuple(fname, filename_t());// 后缀名为空
}
// finally - return a valid base and extension tuple
return std::make_tuple(fname.substr(0, ext_index), fname.substr(ext_index));
}
思考:能否使用std::pair,替换std::tuple存放基础文件名、后缀名?
理论上是可以的,不过没有std::tuple便于扩展多个参数。
另外,std::tuple可以利用std::tie对元组对象进行解包:
filename_t basename, ext;
std::tie(basename, ext) = file_helper::split_by_extension(filename); // 获取基础文件名、扩展名
daily_file_sink类模板
假设我们想在每天的指定时间,创建一个新的log file,附加时间戳到log文件名,该如何进行?
可以为logger装配daily_file_sink。例如,下面调用spdlog::daily_logger_mt的例子,就能每天都创建一个daily_logger对应的log文件:
#include "spdlog/sinks/daily_file_sink.h"
...
// 每天的14:55, 在logs目录下, 创建新的日志文件, 如"daily_logger_2022-11-03"
auto daily_logger = spdlog::daily_logger_mt("daily_logger", "logs/daily", 14, 55);
注意:文件名不能包含冒号
daily_logger_mt是提供给用户创建logger的接口,实际调用了同步工厂方法synchronous_factory::create,创建logger对象:
// factory functions
template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> daily_logger_mt(const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0,
bool truncate = false, uint16_t max_files = 0, const file_event_handlers &event_handlers = {})
{
// daily_file_sink_mt为工厂方法指定要装配的sink类型, 后面的函数参数用于构造sink对象
// 工厂方法会自动将新建的sin对象装配给新建的logger对象, 并用shared_ptr包裹返回给调用者
return Factory::template create<sinks::daily_file_sink_mt>(logger_name, filename, hour, minute, truncate, max_files, event_handlers);
}
daily_file_sink声明式:
/*
* Rotating file sink based on date.
* If truncate != false , the created file will be truncated.
* If max_files > 0, retain only the last max_files and delete previous.
*/
template<typename Mutex, typename FileNameCalc = daily_filename_calculator>
class daily_file_sink final : public base_sink<Mutex>
{
public:
// create daily file sink which rotates on given time
daily_file_sink(filename_t base_filename, int rotation_hour, int rotation_minute, bool truncate = false, uint16_t max_files = 0,
const file_event_handlers &event_handlers = {})
: base_filename_(std::move(base_filename))
, rotation_h_(rotation_hour)
, rotation_m_(rotation_minute)
, file_helper_{event_handlers}
, truncate_(truncate)
, max_files_(max_files)
, filenames_q_()
{
// 检查参数合法性
if (rotation_hour < 0 || rotation_hour > 23 || rotation_minute < 0 || rotation_minute > 59)
{
throw_spdlog_ex("daily_file_sink: Invalid rotation time in ctor");
}
auto now = log_clock::now();
// 根据当前时间, 基础文件名计算最终log文件名
auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now));
file_helper_.open(filename, truncate_);
rotation_tp_ = next_rotation_tp_();
if (max_files_ > 0)
{
init_filenames_q_(); // 初始化指定数量的一组文件名, 存放到文件名环形缓冲区
}
}
// 获取当前open文件名, 实际转发给file_helper_.filename()来实现
filename_t filename();
...
private:
filename_t base_filename_; // 基础文件名
int rotation_h_; // 转档小时
int rotation_m_; // 转档分钟
log_clock::time_point rotation_tp_; // 转档时间点
details::file_helper file_helper_; // 文件工具类
bool truncate_; // 截断文件标志
uint16_t max_files_; // 转档文件最大数量
details::circular_q<filename_t> filenames_q_; // 存放转档文件名的环形缓冲区
};
// 便捷模板实例化类型
using daily_file_sink_mt = daily_file_sink<std::mutex>;
using daily_file_sink_st = daily_file_sink<details::null_mutex>;
using daily_file_format_sink_mt = daily_file_sink<std::mutex, daily_filename_format_calculator>;
using daily_file_format_sink_st = daily_file_sink<details::null_mutex, daily_filename_format_calculator>;
- 计算文件名
有两种计算文件名的方法daily_filename_calculator、daily_filename_format_calculator,以同名函数calc_filename形式,将其封装到不同struct中,便于将其作为模板参数从而参数化。
两种方法区别:
daily_filename_calculator 根据指定时间点now_tm,生成文件名形如basename.YYYY-MM-DD.ext;
daily_filename_format_calculator 根据用户格式串,生成文件名形如
/*
* Generator of daily log file names in format basename.YYYY-MM-DD.ext
*/
struct daily_filename_calculator
{
// Create filename for the form basename.YYYY-MM-DD
static filename_t calc_filename(const filename_t &filename, const tm &now_tm)
{
filename_t basename, ext;
// 切分文件名为基础文件名 + 扩展名
std::tie(basename, ext) = details::file_helper::split_by_extension(filename);
// 最终文件名加上指定时间now_tm的年月日信息
return fmt_lib::format(SPDLOG_FMT_STRING(SPDLOG_FILENAME_T("{}_{:04d}-{:02d}-{:02d}{}")), basename, now_tm.tm_year + 1900,
now_tm.tm_mon + 1, now_tm.tm_mday, ext);
}
};
// 注意filename参数本身包含了格式串信息,
/*
* Generator of daily log file names with strftime format.
* Usages:
* auto sink = std::make_shared<spdlog::sinks::daily_file_format_sink_mt>("myapp-%Y-%m-%d:%H:%M:%S.log", hour, minute);
* auto logger = spdlog::daily_logger_format_mt(loggername, "myapp-%Y-%m-%d:%X.log", hour, minute);
* 模式标志%X 对应"%H:%M:%S", 这样创建的log文件名包含冒号(":"), 可能导致open文件时发生错误: [Errno 22] Invalid argument
*/
struct daily_filename_format_calculator
{
// filename是daily_logger_format_mt的第二个参数, 即格式化字符串
/ 为什么
static filename_t calc_filename(const filename_t &filename, const tm &now_tm)
{
#ifdef SPDLOG_USE_STD_FORMAT
// adapted from fmtlib: https://github.com/fmtlib/fmt/blob/8.0.1/include/fmt/chrono.h#L522-L546
filename_t tm_format;
tm_format.append(filename);
// By appending an extra space we can distinguish an empty result that
// indicates insufficient buffer size from a guaranteed non-empty result
// https://github.com/fmtlib/fmt/issues/2238
tm_format.push_back(' ');
const size_t MIN_SIZE = 10;
filename_t buf;
buf.resize(MIN_SIZE);
for (;;)
{
// 将当前时间now_tm按tm_format格式化后, 存放到buf中
size_t count = strftime(buf.data(), buf.size(), tm_format.c_str(), &now_tm);
if (count != 0)
{
// Remove the extra space.
buf.resize(count - 1);
break;
}
buf.resize(buf.size() * 2);
}
return buf;
#else
// 生成fmt日期格式化字符串, 例如{:%Y-%m-%d}
// generate fmt datetime format string, e.g. {:%Y-%m-%d}.
filename_t fmt_filename = fmt::format(SPDLOG_FMT_STRING(SPDLOG_FILENAME_T("{{:{}}}")), filename);
# if defined(_MSC_VER) && defined(SPDLOG_WCHAR_FILENAMES) // for some reason msvc doesn't allow fmt::runtime(..) with wchar here
return fmt::format(fmt_filename, now_tm);
# else
return fmt::format(SPDLOG_FMT_RUNTIME(fmt_filename), now_tm);
# endif
#endif
}
private:
#if defined __GNUC__
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
// 转发给std::strftime, 根据format指向字符串中格式命令, 把time中保存的时间信息放在str指向的字符串中, 最多向str存放count个字符
// 返回向str指向字符串中放置的字符数
static size_t strftime(char *str, size_t count, const char *format, const std::tm *time)
{
return std::strftime(str, count, format, time);
}
// 处理宽字符版本strftime
static size_t strftime(wchar_t *str, size_t count, const wchar_t *format, const std::tm *time)
{
return std::wcsftime(str, count, format, time);
}
#if defined(__GNUC__)
# pragma GCC diagnostic pop
#endif
};
宏SPDLOG_FILENAME_T 用于将字面量字符串声明为适用于文件名的类型,即普通字符串,或宽字符串(根据具体平台配置选择);
宏SPDLOG_FMT_STRING 用于formatter特例化中检查用户定义的字符串是否为constexpr,要求C++14,C++11中为空操作。
详见fmt官网:https://fmt.dev/latest/api.html
// SPDLOG_FILENAME_T处理宽字符类型
#if defined(_WIN32) && defined(SPDLOG_WCHAR_FILENAMES)
using filename_t = std::wstring;
// allow macro expansion to occur in SPDLOG_FILENAME_T
# define SPDLOG_FILENAME_T_INNER(s) L##s
# define SPDLOG_FILENAME_T(s) SPDLOG_FILENAME_T_INNER(s)
#else
using filename_t = std::string;
# define SPDLOG_FILENAME_T(s) s
#endif
// FMT库类型检查
#if !defined(SPDLOG_USE_STD_FORMAT) && FMT_VERSION >= 80000 // backward compatibility with fmt versions older than 8
# define SPDLOG_FMT_RUNTIME(format_string) fmt::runtime(format_string) // FMT运行时格式化字符串检查
# define SPDLOG_FMT_STRING(format_string) FMT_STRING(format_string) // FMT编译时格式化字符串检查
# if defined(SPDLOG_WCHAR_FILENAMES) || defined(SPDLOG_WCHAR_TO_UTF8_SUPPORT) // 宽字符文件名处理
# include <spdlog/fmt/xchar.h>
# endif
#else
# define SPDLOG_FMT_RUNTIME(format_string) format_string
# define SPDLOG_FMT_STRING(format_string) format_string
#endif
- 下一个转档时间点
next_rotation_tp_基于当前日期 + 用户指定的转档时间小时、分钟,计算一天以后的时间点(日期 + 小时、分钟),时间点用chrono::system_clock::time_point结构体表示。
log_clock::time_point next_rotation_tp_()
{
// 获取当前日期, 由用户指定转档小时、分钟
auto now = log_clock::now();
tm date = now_tm(now);
date.tm_hour = rotation_h_;
date.tm_min = rotation_m_;
date.tm_sec = 0;
auto rotation_time = log_clock::from_time_t(std::mktime(&date));
if (rotation_time > now)
{
return rotation_time;
}
return {rotation_time + std::chrono::hours(24)};
}
now_tm是一个自定义的private工具函数,用于将时间点信息由time_point类型转化为tm类型。
tm now_tm(log_clock::time_point tp)
{
time_t tnow = log_clock::to_time_t(tp);
return spdlog::details::os::localtime(tnow); // 将tnow转化为tm类型的本地时间
}
- 初始化一组文件名
当指定最大文件数max_files_ > 0时,就生成一组文件名,包含的时间信息逐个往前推24h。生成的一组文件名,存放到文件名环形数组filenames_q_中。
异常处理:如果文件名对应文件不存在,就停止生成。
private:
void init_filenames_q_()
{
using details::os::path_exists;
filenames_q_ = details::circular_q<filename_t>(static_cast<size_t>(max_files_)); // 创建环形缓冲区, 用于存放文件名
std::vector<filename_t> filenames;
auto now = log_clock::now();
// 生成一组文件名
while (filenames.size() < max_files_)
{
auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now));
if (!path_exists(filename)) // 文件名不存在, 就停止
{
break;
}
filenames.emplace_back(filename);
now -= std::chrono::hours(24); // 时间往前24小时
}
// 将文件名从vector存回环形缓冲区
for (auto iter = filenames.rbegin(); iter != filenames.rend(); ++iter)
{
filenames_q_.push_back(std::move(*iter));
}
}
- 将log消息写到log文件(重写virtual函数)
sink_it_是父类base_sink提供的算法框架,在log()中调用,用来实现将log消息写到目标log文件,必须由子类实现。
类似地,还有flush_,不过很简单,直接转发给file_helper_.flush,这里不赘述。
protected:
void sink_it_(const details::log_msg &msg) override
{
auto time = msg.time;
bool should_rotate = time >= rotation_tp_; // 根据log消息中包含的当前时间判断是否应当转档, 将内容存放到新文件
if (should_rotate)
{
auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(time));
file_helper_.open(filename, truncate_); // open新的文件
rotation_tp_ = next_rotation_tp_(); // 更新下一个转档时间点
}
memory_buf_t formatted;
base_sink<Mutex>::formatter_->format(msg, formatted); // 将log消息格式化, 即根据pattern flag将参数解析, 最终字符串存放二进制内存formatted中
file_helper_.write(formatted); // 将缓存formatted内容写到文件
// 只有发生转档并且有最大文件限制时, 就删除最老的转档文件
// Do the cleaning only at the end because it might throw on failure.
if (should_rotate && max_files_ > 0)
{
delete_old_();
}
}
- 删除最老的转档文件
// Delete the file N rotations ago.
// Throw spdlog_ex on failure to delete the old file.
void delete_old_()
{
using details::os::filename_to_str;
using details::os::remove_if_exists;
filename_t current_file = file_helper_.filename(); // 获取当前正在写的log文件
if (filenames_q_.full()) // 文件名缓存满
{
auto old_filename = std::move(filenames_q_.front()); // 最老的文件名
filenames_q_.pop_front(); // 从环形队列中删除一个元素, 即最老的文件名
// 判断文件是否存在, 如果存在则删除文件
bool ok = remove_if_exists(old_filename) == 0;
if (!ok) // 文件不存在, 说明发生异常. 可能被意外删除, 或者环形队列被异常修改
{
filenames_q_.push_back(std::move(current_file)); // 将当前文件加入环形队列
throw_spdlog_ex("Failed removing daily file " + filename_to_str(old_filename), errno);
}
}
filenames_q_.push_back(std::move(current_file));
}
dist_sink类模板
dist_sink基础自base_sink,是一个sink复用器,包含一组sinks,当log调用时,可分发给所有sink。
dist_sink声明式:
template<typename Mutex>
class dist_sink : public base_sink<Mutex>
{
public:
dist_sink() = default;
explicit dist_sink(std::vector<std::shared_ptr<sink>> sinks)
: sinks_(sinks)
{}
// 因为对应类类底层文件资源, 因此禁止拷贝
dist_sink(const dist_sink &) = delete;
dist_sink &operator=(const dist_sink &) = delete;
...
protected:
...
std::vector<std::shared_ptr<sink>> sinks_;
};
// 便捷类型
using dist_sink_mt = dist_sink<std::mutex>; // 线程安全版本
using dist_sink_st = dist_sink<details::null_mutex>; // 非线程安全版本
- 实现pure virtual函数
sink_it_ 将log消息写到目标文件。dist_sink的实现则是将log消息转交给每个sink对象来处理。
flush_ 将log消息从缓存冲刷到目标文件。dist_sink的实现也是交给每个sink对象来处理。
protected:
void sink_it_(const details::log_msg &msg) override
{
for (auto &sink : sinks_)
{
if (sink->should_log(msg.level))
{
sink->log(msg);
}
}
}
void flush_() override
{
for (auto &sink : sinks_)
{
sink->flush();
}
}
- sink数组操作
对sink数组进行增删改查,属于public接口,需要加锁以确保线程安全。
public:
// 增
void add_sink(std::shared_ptr<sink> sink)
{
std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
sinks_.push_back(sink);
}
// 删
void remove_sink(std::shared_ptr<sink> sink)
{
std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
sinks_.erase(std::remove(sinks_.begin(), sinks_.end(), sink), sinks_.end());
}
// 改
void set_sinks(std::vector<std::shared_ptr<sink>> sinks)
{
std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
sinks_ = std::move(sinks);
}
// 查
std::vector<std::shared_ptr<sink>> &sinks()
{
return sinks_;
}
- pattern、formatter操作
因为dist_sink内含多个子sink,而父类的pattern操作都是针对自身(单个sink)的,因此需要重写pattern操作,将pattern和formatter转发给子sink对象。
// 设置模式
void set_pattern_(const std::string &pattern) override
{
set_formatter_(details::make_unique<spdlog::pattern_formatter>(pattern));
}
// 设置格式
void set_formatter_(std::unique_ptr<spdlog::formatter> sink_formatter) override
{
base_sink<Mutex>::formatter_ = std::move(sink_formatter);
for (auto &sink : sinks_)
{
sink->set_formatter(base_sink<Mutex>::formatter_->clone());
}
}
dup_filter_sink 类模板
dup_filter_sink 用于过滤一定时间内相同的log消息,只会写一条,不会都写到log文件。
例如,下面这段代码利用dup_filter_sink过滤相同的log消息。
#include <spdlog/sinks/dup_filter_sink.h>
int main() {
auto dup_filter = std::make_shared<dup_filter_sink_st>(std::chrono::seconds(5));
dup_filter->add_sink(std::make_shared<stdout_color_sink_mt>());
spdlog::logger l("logger", dup_filter);
l.info("Hello");
l.info("Hello");
l.info("Hello");
l.info("Different Hello");
}
运行输出:
[2019-06-25 17:50:56.511] [logger] [info] Hello
[2019-06-25 17:50:56.512] [logger] [info] Skipped 3 duplicate messages..
[2019-06-25 17:50:56.512] [logger] [info] Different Hello
可以看到,过滤了2个相同的log消息(正文为“Hello”)。打印的为什么是“3”?因为第1次打印消息并没有专门处理。
dup_filter_sink 声明式:
template<typename Mutex>
class dup_filter_sink : public dist_sink<Mutex>
{
public:
template<class Rep, class Period>
explicit dup_filter_sink(std::chrono::duration<Rep, Period> max_skip_duration)
: max_skip_duration_{max_skip_duration}
{}
protected:
std::chrono::microseconds max_skip_duration_; // 过滤时间,单位:微秒
log_clock::time_point last_msg_time_; // 上一次log消息时间点
std::string last_msg_payload_; // log消息载荷,即用户写的文本内容
size_t skip_counter_ = 0; // 过滤次数
void sink_it_(const details::log_msg &msg) override; // 父类dist_sink定义的virtual函数
...
};
using dup_filter_sink_mt = dup_filter_sink<std::mutex>;
using dup_filter_sink_st = dup_filter_sink<details::null_mutex>;
- 实现pure virtual函数
sink_it_ 是向目标文件写log消息。dup_filter_sink的做法是,先判断与规定时间内的上一次log消息是否相同,如果相同就过滤掉;如果就先写之前的过滤信息,然后。
过滤重复log消息,并不是悄无声息的,而是会写一个"Skipped n duplicate messages.."的提示信息。
protected:
void sink_it_(const details::log_msg &msg) override
{
bool filtered = filter_(msg); // false表示应该过滤掉, true表示不应该
if (!filtered)
{
skip_counter_ += 1;
return;
}
// 过滤了重复log消息, 但应产生对应的过滤信息
// log the "skipped.." message
if (skip_counter_ > 0)
{
char buf[64];
auto msg_size = ::snprintf(buf, sizeof(buf), "Skipped %u duplicate messages..", static_cast<unsigned>(skip_counter_));
if (msg_size > 0 && static_cast<size_t>(msg_size) < sizeof(buf))
{
// 调用父类sink_it_ 将log消息写入sink对象对应的目标文件, 因为是virtual函数, 所以需要显式调用
details::log_msg skipped_msg{msg.logger_name, level::info, string_view_t{buf, static_cast<size_t>(msg_size)}};
dist_sink<Mutex>::sink_it_(skipped_msg);
}
}
// 通过父类sink_it_ 将log消息写入sink对象
// log current message
dist_sink<Mutex>::sink_it_(msg);
// 更新上一次消息状态
last_msg_time_ = msg.time;
skip_counter_ = 0;
last_msg_payload_.assign(msg.payload.data(), msg.payload.data() + msg.payload.size());
}
ringbuffer_sink类模板
通常,sink的目标是一个文件,而ringbuffer_sink的目标是一个环形缓冲区,即内存。如果想把log消息写到内存中缓存起来,那么可以使用ringbuffer_sink。
ringbuffer_sink声明式:
/*
* Ring buffer sink
*/
template<typename Mutex>
class ringbuffer_sink final : public base_sink<Mutex>
{
public:
// 构造者指定环形缓冲区大小
explicit ringbuffer_sink(size_t n_items)
: q_{n_items}
{}
std::vector<details::log_msg_buffer> last_raw(size_t lim = 0);
std::vector<std::string> last_formatted(size_t lim = 0);
...
private:
details::circular_q<details::log_msg_buffer> q_; // sink的目标, 即一个环形缓冲区
};
using ringbuffer_sink_mt = ringbuffer_sink<std::mutex>;
using ringbuffer_sink_st = ringbuffer_sink<details::null_mutex>;
- pure virtual函数
sink_it_ 实现是简单的将log消息插入到环形缓冲区末尾;
flush_ 则实现为一个空函数,因为没有内容需要写到文件。
protected:
void sink_it_(const details::log_msg &msg) override
{
q_.push_back(details::log_msg_buffer{msg}); // 调用的是vector<>::push_back(log_msg_buffer &&)版本
}
void flush_() override {}
- 查询
作为一个目标是环形缓冲区的sink,不仅只是作为输入接收log消息,而且还能作为输出,供调用者查询。ringbuffer_sink 提供了2个这样的接口:last_raw 用于读取环形缓冲区最近的一些原始类型的log消息(log_msg_buffer),last_formatted 用与读取最近一些格式化后的log消息(std::string),返回结果都是用std::vector管理。
public:
// 最近的lim个原始log消息, lim值为0代表不限制个数
std::vector<details::log_msg_buffer> last_raw(size_t lim = 0)
{
std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
auto items_available = q_.size(); // 环形队列元素个数
auto n_items = lim > 0 ? (std::min)(lim, items_available) : items_available;
std::vector<details::log_msg_buffer> ret;
ret.reserve(n_items);
// 获取最近的n_items个元素: 从队列考前部分往后访问, 头部是老的数据, 尾部是新数据
for (size_t i = (items_available - n_items); i < items_available; i++)
{
ret.push_back(q_.at(i));
}
return ret;
}
// 最近的lim个格式化的log消息(字符串类型), lim值为0代表不限制个数
std::vector<std::string> last_formatted(size_t lim = 0)
{
std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
auto items_available = q_.size(); // 环形队列元素个数
auto n_items = lim > 0 ? (std::min)(lim, items_available) : items_available;
std::vector<std::string> ret;
ret.reserve(n_items);
// 获取最近的n_items个元素: 从队列考前部分往后访问, 头部是老的数据, 尾部是新数据
for (size_t i = (items_available - n_items); i < items_available; i++)
{
// 将log消息进行格式化, 从log_msg_buffer类型转化为std::string类型
memory_buf_t formatted;
base_sink<Mutex>::formatter_->format(q_.at(i), formatted);
ret.push_back(std::move(SPDLOG_BUF_TO_STRING(formatted)));
}
return ret;
}
注:public需要加锁确保线程安全。
其中,宏SPDLOG_BUF_TO_STRING用于将memory_buf_t转化为std::string;当然,如果是宽字符,就转化为std::wstring。
rotating_file_sink类模板
rotating_file_sink用于log文件转档,跟daily_file_sink区别是:rotating_file_sink可用于指定log文件的大小、个数,从而进行log文件转档;daily_file_sink 只是简单的每天在指定时间点创建新log文件。
rotating_file_sink声明式:
//
// Rotating file sink based on size
//
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(); // 获取当前log文件名
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_();
// 对src所指文件重命名为target
// 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_; // 文件操作工具
};
// 便捷类型
using rotating_file_sink_mt = rotating_file_sink<std::mutex>; // 线程安全版本
using rotating_file_sink_st = rotating_file_sink<details::null_mutex>; // 非线程安全版本
为何会有filename()接口?
因为转档文件过程中,可能会产生log文件,因此有必要区分当前log文件名与已经转档的log文件名。
- 构造函数
rotating_file_sink采用RAII方式管理log文件,构造时open,析构时close。构造函数要求文件最大尺寸max_size必须非0,最大文件数max_files <= 200000。
template<typename Mutex>
SPDLOG_INLINE rotating_file_sink<Mutex>::rotating_file_sink(
filename_t base_filename, std::size_t max_size, std::size_t max_files, bool rotate_on_open, const file_event_handlers &event_handlers)
: base_filename_(std::move(base_filename))
, max_size_(max_size)
, max_files_(max_files)
, file_helper_{event_handlers}
{
if (max_size == 0)
{
throw_spdlog_ex("rotating sink constructor: max_size arg cannot be zero");
}
if (max_files > 200000)
{
throw_spdlog_ex("rotating sink constructor: max_files arg cannot exceed 200000");
}
// 以append方式open当前log文件
file_helper_.open(calc_filename(base_filename_, 0));
// 获取文件大小
current_size_ = file_helper_.size(); // expensive. called only once
if (rotate_on_open && current_size_ > 0)
{ // 如果open文件时就转档, 就清空current_size_
rotate_();
current_size_ = 0;
}
}
- calc_filename 计算转档文件名
转档文件名格式:基础文件名.索引号.扩展名。calc_filename正是根据用户通过的文件名、索引号、扩展名,来生成转档文件名。例如,基础文件名为"mylog",索引号从0~n-1,扩展名为"txt",则通过rotating_file_sink生成的log文件名像这样:
mylog.txt
mylog.1.txt
mylog.2.txt
mylog.3.txt
...
mylog.{n-1}.txt
calc_filename实现很简单,利用file_helper::split_by_extension对用户提供的文件名进行切分,然后通过fmt_lib::format进行组合,得到想要的转档文件名。
问题:为何要将rotating_file_sink实现为static类函数,而不是普通的成员函数,或外部static函数?
因为calc_filename需要在构造函数中调用,而构造函数中是不能调用普通成员函数的,因为此时对象尚未构造完成。事实上,个人认为可以是文件范围内的static函数或global函数,因为calc_filename并未访问rotating_file_sink的静态数据成员。
- pure virtual函数
rotating_file_sink将sink_it_ 实现为先判断添加格式化后的log消息后的log文件大小,如果超过阈值限值就转档(rotate_()),然后把log消息写到新文件中;如果没有超过,就不需要转档,直接log消息。
template<typename Mutex>
SPDLOG_INLINE void rotating_file_sink<Mutex>::sink_it_(const details::log_msg &msg)
{
memory_buf_t formatted;
base_sink<Mutex>::formatter_->format(msg, formatted);
auto new_size = current_size_ + formatted.size(); // log文件写本条log消息后的新大小
// 如果新大小超过限值, 就进行转档
// rotate if the new estimated file size exceeds max size.
// rotate only if the real size > 0 to better deal with full disk (see issue #2261).
// we only check the real size when new_size > max_size_ because it is relatively expensive.
if (new_size > max_size_)
{
file_helper_.flush();
if (file_helper_.size() > 0)
{
rotate_(); // 转档
new_size = formatted.size();
}
}
file_helper_.write(formatted);
current_size_ = new_size;
}
template<typename Mutex>
SPDLOG_INLINE void rotating_file_sink<Mutex>::flush_()
{
file_helper_.flush();
}
- rotate_转档
rotate_
是rotating_file_sink核心功能,转档时需要删除索引号最大(max_files_)的文件,即最老的log文件,而索引号较小的文件名会重命名:索引号+1。
// Rotate files:
// log.txt -> log.1.txt
// log.1.txt -> log.2.txt
// log.2.txt -> log.3.txt
// log.3.txt -> delete
template<typename Mutex>
SPDLOG_INLINE void rotating_file_sink<Mutex>::rotate_()
{
using details::os::filename_to_str;
using details::os::path_exists;
file_helper_.close(); // 转档第一步, 关闭当前已经打开的log文件
// 从索引号较大到较小进行遍历, 方便文件名索引增大
for (auto i = max_files_; i > 0; --i)
{
// 将i-1索引对应文件名的索引号+1, 变成i
filename_t src = calc_filename(base_filename_, i - 1);
if (!path_exists(src))
{
continue;
}
filename_t target = calc_filename(base_filename_, i);
// 重命名src文件为target
if (!rename_file_(src, target)) // error
{
// 由于Windows防病毒机制,重命名可能失败,需要延时一小会儿再重试一次
// if failed try again after a small delay.
// this is a workaround to a windows issue, where very high rotation
// rates can cause the rename to fail with permission denied (because of antivirus?).
details::os::sleep_for_millis(100); // 延时100ms
if (!rename_file_(src, target))
{
// 如果重命名失败,就以截断方式打开文件
file_helper_.reopen(true); // truncate the log file anyway to prevent it to grow beyond its limit!
current_size_ = 0;
throw_spdlog_ex("rotating_file_sink: failed renaming " + filename_to_str(src) + " to " + filename_to_str(target), errno);
}
}
}
file_helper_.reopen(true); // 截断方式重新打开当前log文件
}
- rename_file_ 重命名文件
rename_file_ 重命名文件,方法是先删除目标文件, 然后重命名源文件为目标文件。
// 将文件名src_filename重命名为target_filename
// delete the target if exists, and rename the src file to target
// return true on success, false otherwise.
template<typename Mutex>
SPDLOG_INLINE bool rotating_file_sink<Mutex>::rename_file_(const filename_t &src_filename, const filename_t &target_filename)
{
// try to delete the target file in case it already exists.
(void)details::os::remove(target_filename);
return details::os::rename(src_filename, target_filename) == 0;
}
spdlog为os::remove、rename针对普通字符串、宽字符串实现了2个版本:文件名为普通字符串时,转发给std::remove、std::rename;文件名为宽字符串时,转发给_wremove、_wrename。
// 删除指定文件filename
SPDLOG_INLINE int remove(const filename_t &filename) SPDLOG_NOEXCEPT
{
#if defined(_WIN32) && defined(SPDLOG_WCHAR_FILENAMES) // 宽字符
return ::_wremove(filename.c_str());
#else
return std::remove(filename.c_str());
#endif
}
// 重命名文件filename1为filename2
SPDLOG_INLINE int rename(const filename_t &filename1, const filename_t &filename2) SPDLOG_NOEXCEPT
{
#if defined(_WIN32) && defined(SPDLOG_WCHAR_FILENAMES) // 宽字符
return ::_wrename(filename1.c_str(), filename2.c_str());
#else
return std::rename(filename1.c_str(), filename2.c_str());
#endif
}
syslog_sink类模板
其他sink将log消息写到目标文件或内存,而syslog_sink则将log消息交给一个系统服务进程syslog(POSIX,Windows不支持),进而写到系统日志文件(由syslog完成)。syslog_sink采用RAII方式管理syslog资源,构造即调用openlog打开syslog,析构即调用closelog关闭syslog。
syslog_sink声明式:
/**
* Sink that write to syslog using the `syscall()` library call.
*/
template<typename Mutex>
class syslog_sink : public base_sink<Mutex>
{
public:
syslog_sink(std::string ident, int syslog_option, int syslog_facility, bool enable_formatting)
: enable_formatting_{enable_formatting}
, syslog_levels_{{/* spdlog::level::trace */ LOG_DEBUG,
/* spdlog::level::debug */ LOG_DEBUG,
/* spdlog::level::info */ LOG_INFO,
/* spdlog::level::warn */ LOG_WARNING,
/* spdlog::level::err */ LOG_ERR,
/* spdlog::level::critical */ LOG_CRIT,
/* spdlog::level::off */ LOG_INFO}}
, ident_{std::move(ident)}
{
// set ident to be program name if empty
::openlog(ident_.empty() ? nullptr : ident_.c_str(), syslog_option, syslog_facility);
}
~syslog_sink() override
{
::closelog();
}
// 因为对应了底层系统资源, 因此禁用拷贝
syslog_sink(const syslog_sink &) = delete;
syslog_sink &operator=(const syslog_sink &) = delete;
protected:
void sink_it_(const details::log_msg &msg) override;
void flush_() override;
bool enable_formatting_ = false;
private:
using levels_array = std::array<int, 7>;
levels_array syslog_levels_; // syslog日志等级数组
// must store the ident because the man says openlog might use the pointer as
// is and not a string copy
const std::string ident_;
//
// Simply maps spdlog's log level to syslog priority level.
//
int syslog_prio_from_level(const details::log_msg &msg) const;
};
// 便捷类型
using syslog_sink_mt = syslog_sink<std::mutex>; // 线程安全版本
using syslog_sink_st = syslog_sink<details::null_mutex>; // 非现线程安全版本
- pure virtual函数实现
sink_it_ 实现为对log消息格式化(根据需要),然后将内容通过syslog()接口转交给syslog服务。
flush_ 实现为空,因为不涉及本进程下的文件操作。
protected:
void sink_it_(const details::log_msg &msg) override
{
string_view_t payload;
memory_buf_t formatted;
if (enable_formatting_)
{
base_sink<Mutex>::formatter_->format(msg, formatted);
payload = string_view_t(formatted.data(), formatted.size()); // 格式化log消息
}
else
{
payload = msg.payload; // 直接赋值为原始的用户消息(而非格式化的log消息)
}
size_t length = payload.size();
// limit to max int
if (length > static_cast<size_t>(std::numeric_limits<int>::max()))
{
length = static_cast<size_t>(std::numeric_limits<int>::max());
}
// 将log消息转交给syslog,注意需要将日志等级进行转换
::syslog(syslog_prio_from_level(msg), "%.*s", static_cast<int>(length), payload.data());
}
void flush_() override {}
- syslog_prio_from_level 日志等级转换
用户写log消息时,使用的是spdlog日志等级,而syslog需要的是自身的日志等级,因此需要转换。syslog_prio_from_level负责将日志等级从spdlog日志等级转换为syslog日志等级。
//
// Simply maps spdlog's log level to syslog priority level.
//
int syslog_prio_from_level(const details::log_msg &msg) const
{
return syslog_levels_.at(static_cast<levels_array::size_type>(msg.level));
}
两种日志等级对应关系,在syslog_levels_构造的注释中,已经详细说明:
syslog_levels_{{/* spdlog::level::trace */ LOG_DEBUG,
/* spdlog::level::debug */ LOG_DEBUG,
/* spdlog::level::info */ LOG_INFO,
/* spdlog::level::warn */ LOG_WARNING,
/* spdlog::level::err */ LOG_ERR,
/* spdlog::level::critical */ LOG_CRIT,
/* spdlog::level::off */ LOG_INFO}}
stdout_sink_base类模板
stdout_sink_base是另一个系列的sink派生类,父类是sink而非base_sink,用于向控制台(stdout/stderr)输出log消息。
stdout_sink_base声明式:
template<typename ConsoleMutex>
class stdout_sink_base : public sink
{
public:
using mutex_t = typename ConsoleMutex::mutex_t;
explicit stdout_sink_base(FILE *file);
~stdout_sink_base() override = default;
// 禁用copy、move
stdout_sink_base(const stdout_sink_base &other) = delete;
stdout_sink_base(stdout_sink_base &&other) = delete;
stdout_sink_base &operator=(const stdout_sink_base &other) = delete;
stdout_sink_base &operator=(stdout_sink_base &&other) = delete;
// 重写sink的pure virtual函数
void log(const details::log_msg &msg) override;
void flush() override;
void set_pattern(const std::string &pattern) override;
void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) override;
protected:
mutex_t &mutex_; // 为什么是引用类型?
FILE *file_;
std::unique_ptr<spdlog::formatter> formatter_;
#ifdef _WIN32
HANDLE handle_;
#endif // WIN32
};
- 构造函数
非Windows平台需要传入stdout/stderr作为控制台文件指针,而Windows平台需要获取句柄,然后对句柄进行文件IO操作。
template<typename ConsoleMutex>
SPDLOG_INLINE stdout_sink_base<ConsoleMutex>::stdout_sink_base(FILE *file)
: mutex_(ConsoleMutex::mutex()) // 从模板参数ConsoleMutex中萃取出mutex对象, 绑定到引用mutex_
, file_(file)
, formatter_(details::make_unique<spdlog::pattern_formatter>()) // 缺省的formatter
{
#ifdef _WIN32 // Windows
// get windows handle from the FILE* object
handle_ = reinterpret_cast<HANDLE>(::_get_osfhandle(::_fileno(file_))); // 获得file对应句柄
// 文件指针及句柄判断
// don't throw to support cases where no console is attached,
// and let the log method to do nothing if (handle_ == INVALID_HANDLE_VALUE).
// throw only if non stdout/stderr target is requested (probably regular file and not console).
if (handle_ == INVALID_HANDLE_VALUE && file != stdout && file != stderr)
{
throw_spdlog_ex("spdlog::stdout_sink_base: _get_osfhandle() failed", errno);
}
#endif // WIN32
}
- pure function实现
stdout_sink_base并非base_sink派生类,无需实现sink_it_, flush_,不过需要实现sink的pure virtual函数:log、flush、set_pattern、set_formatter。
log()实现为先将log消息格式化为字符串,然后write进目标控制台。
// 接收用户log消息,写入目标文件
template<typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::log(const details::log_msg &msg)
{
#ifdef _WIN32 // Windows
if (handle_ == INVALID_HANDLE_VALUE)
{
return;
}
std::lock_guard<mutex_t> lock(mutex_); // 取得锁
memory_buf_t formatted;
formatter_->format(msg, formatted); // 格式化log消息
::fflush(file_); // flush in case there is something in this file_ already
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 // non-Windows
std::lock_guard<mutex_t> lock(mutex_);
memory_buf_t formatted;
formatter_->format(msg, formatted);
::fwrite(formatted.data(), sizeof(char), formatted.size(), file_);
::fflush(file_); // flush every line to terminal
#endif // WIN32
}
flush()实现为转发给系统调用fflush。
template<typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::flush()
{
std::lock_guard<mutex_t> lock(mutex_);
fflush(file_);
}
set_pattern()实现为通过模式字符串构造一个pattern_formatter。
template<typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::set_pattern(const std::string &pattern)
{
std::lock_guard<mutex_t> lock(mutex_);
formatter_ = std::unique_ptr<spdlog::formatter>(new pattern_formatter(pattern));
}
set_formatter()实现为由调用者传入一个formatter对象。
template<typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter)
{
std::lock_guard<mutex_t> lock(mutex_);
formatter_ = std::move(sink_formatter);
}
- 线程安全
stdout_sink_base通过mutex_实现线程安全,每个public接口中,有对需要保护的数据时,对mutex_加锁。而mutex_类型为mutex_t &(即ConsoleMutex::mutex_t),从模板参数ConsoleMutex萃取mutex_t特性而来的,ConsoleMutex::mutex_t可以是线程安全的互斥锁(如console_mutex),也可以是非线程安全的空锁(console_nullmutex)。
思考:为什么mutex_ 是引用类型,而非类似于base_sink::mutex_的非引用类型?
因为控制台不同于普通文件,一个进程通常只有一个全局的控制台用于输出,因此所有的stdout_sink_base及其派生类共用一个控制台,也就需要共用一个互斥锁。
下面console_mutex、console_nullmutex类型实现:
1)特性mutex_t;
2)唯一锁实例mutex();
// 控制台互斥锁
struct console_mutex
{
using mutex_t = std::mutex; // 标准库互斥锁
static mutex_t &mutex()
{
static mutex_t s_mutex; // 确保全局共享一个锁
return s_mutex;
}
};
// 控制台空锁
struct console_nullmutex
{
using mutex_t = null_mutex; // 自定义空锁
static mutex_t &mutex()
{
static mutex_t s_mutex; // 确保全局共享一个锁
return s_mutex;
}
};
用于stdout、stderr的两个派生类模板
stdout_sink
template<typename ConsoleMutex>
class stdout_sink : public stdout_sink_base<ConsoleMutex>
{
public:
stdout_sink();
};
template<typename ConsoleMutex>
class stderr_sink : public stdout_sink_base<ConsoleMutex>
{
public:
stderr_sink();
};
// 便捷类型
using stdout_sink_mt = stdout_sink<details::console_mutex>;
using stdout_sink_st = stdout_sink<details::console_nullmutex>;
using stderr_sink_mt = stderr_sink<details::console_mutex>;
using stderr_sink_st = stderr_sink<details::console_nullmutex>;
stdout_sink、stderr_sink的构造也非常简单,分别用stdout、stderr文件指针来构造基类对象,这样stdout_sink_base::file_ 就能保存控制台文件指针。
// stdout sink
template<typename ConsoleMutex>
SPDLOG_INLINE stdout_sink<ConsoleMutex>::stdout_sink()
: stdout_sink_base<ConsoleMutex>(stdout)
{}
// stderr sink
template<typename ConsoleMutex>
SPDLOG_INLINE stderr_sink<ConsoleMutex>::stderr_sink()
: stdout_sink_base<ConsoleMutex>(stderr)
{}
注意:stdout、stderr文件的open、close并不由stdout_sink_base或其派生类决定,因此调用者需要确保其生产周期。
spdlog库系列:spdlog库笔记汇总