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::log()接收log消息:

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::sink_it_将msg进行格式化(format)后转换为二进制数据,然后通过工具类file_helper的write()写入目标文件,其实现如下:

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_sink的派生类(类模板),提供文件操作,写log消息到指定文件的基本操作。如果只是想拥有简单的写log消息到文件的功能,那么可使用该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、stderr_sink作为stdout_sink_base的派生类模板,分别用于stdout输出、stderr输出。

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库笔记汇总

posted @ 2022-11-08 19:21  明明1109  阅读(2709)  评论(0编辑  收藏  举报