添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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