spdlog源码学习(上)

spdlog源码学习(上)

写在前面
我使用的spdlog版本是1.10.0,spdlog的最新版可以在https://github.com/gabime/spdlog找到。本文相关的代码和笔记可以在我的github仓库: https://github.com/KuiH/read-spdlog1.10.0找到。仓库中的代码有我自己加的注释。
由于我本人也是出于学习的目的想要读读这份源码,并且我还很菜,所以下面的内容难免有出错的地方,欢迎大家在评论区指出错误,或者私信我也可以!

一、开端

1.1. 总览

//选自 https://www.cnblogs.com/shuqin/p/12214439.html
spdlog
    ├─example  用法代码
    ├─include  实现目录
    │  └─spdlog
    │      ├─details  功能函数目录
    │      ├─fmt  {fmt} 库目录
    │      ├─sinks  落地文件格式实现
    │      └─*.h    异步模式,日志库接口等实现
    ├─src  .cpp 文件,组成编译模块生成静态库使用
    ├─test  测试代码

别人画的类图(spdlog源码分析-整体框架_spdlog架构_注定会的博客-CSDN博客):

img

1.2. level

日志的级别在include\spdlog\common.h中,由枚举level_enum给出。由低到高为trace debug info warn err critical off

相关主要函数有to_string_viewfrom_str,功能分别为根据枚举获取字符串和根据字符串获取枚举值。

这些内容都在namespace level

二、example

这节简单看一下example。

几点发现:

  1. 支持格式化、多线程、异步、多输出目标、计时、自定义类型格式化、自定义异常信息处理等功能
  2. spdlog::[info/warn/…]使用的是默认的logger作为输出
  3. spdlog::xxx_xxx_[mt/st]自己新建logger时用的api。

下阶段目标是看懂默认的输出(以info为例)。

输出一条日志的过程大致为:spdlog::info -> details::registry中的默认logger.info -> 构造details::log_msg -> logger::log_it_ -> 该logger的所有sink.log

看起来要先弄清 details::log_msg和sink。

三、details::log_msg

3.1. source_loc结构体

位于include\spdlog\common.h。记录了打日志的地方所在的文件名、行数、函数名

struct source_loc
{
    SPDLOG_CONSTEXPR source_loc() = default; 
    SPDLOG_CONSTEXPR source_loc(const char *filename_in, int line_in, const char *funcname_in)
        : filename{filename_in}
        , line{line_in}
        , funcname{funcname_in}
    {}

    SPDLOG_CONSTEXPR bool empty() const SPDLOG_NOEXCEPT
    {
        return line == 0;
    }
    const char *filename{nullptr};
    int line{0};
    const char *funcname{nullptr};
};

SPDLOG_CONSTEXPR宏在我的环境下被扩展成了constexpr

constexpr构造函数,把这个类变为一个"字面值常量类"https://blog.csdn.net/craftsman1970/article/details/80244873。constexpr构造函数的函数体一般为空,使用初始化列表或者其他的constexpr构造函数初始化所有数据成员。

constexpr修饰其它函数,将运算尽量放在编译阶段。函数只能有一个return语句

= default 启用默认的构造函数

SPDLOG_NOEXCEPT宏在我的环境下被扩展成了noexcept。该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序

3.2. log_msg

构造函数中提供了多种构造。成员如下,包括logger名、消息的级别、线程id、位置等

    string_view_t logger_name;
    level::level_enum level{level::off};
    log_clock::time_point time; //using log_clock = std::chrono::system_clock; time_point表示一个时间点
    size_t thread_id{0};

    // wrapping the formatted text with color (updated by pattern_formatter).
    mutable size_t color_range_start{0};
    mutable size_t color_range_end{0};

    source_loc source;
    string_view_t payload; //存储的是msg

构造时如果不指定时间,默认为当前时间。

四、sinks

所有的sink都在include\spdlog\sinks目录下。

4.1. sink

sink.h中的sink类为纯虚的抽象类。其中待实现的接口如下:

    virtual void log(const details::log_msg &msg) = 0;
    virtual void flush() = 0;
    virtual void set_pattern(const std::string &pattern) = 0;
    virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) = 0;

已实现的接口如下:

    void set_level(level::level_enum log_level);
    level::level_enum level() const;
    bool should_log(level::level_enum msg_level) const;

should_log实现如下:

SPDLOG_INLINE bool spdlog::sinks::sink::should_log(spdlog::level::level_enum msg_level) const
{
    // std::memory_order_relaxed: 仅仅保证load()和store()是原子操作,除此之外,不提供任何跨线程的同步
    return msg_level >= level_.load(std::memory_order_relaxed);
}

传入的level大于等于sink的level时则通过。

有成员:

level_t level_{level::trace}; // using level_t = std::atomic<int>;
//可见sink的默认级别为trace(级别0)

4.2. base_sink

template<typename Mutex>
class SPDLOG_API base_sink : public sink
{
public:
    base_sink();
    // explicit禁止构造函数被用于隐式转换
    explicit base_sink(std::unique_ptr<spdlog::formatter> formatter);
    ~base_sink() override = default;

    base_sink(const base_sink &) = delete;
    base_sink(base_sink &&) = delete;

    base_sink &operator=(const base_sink &) = delete;
    base_sink &operator=(base_sink &&) = delete;

    // 下面这几个是继承来的纯虚函数。final阻止了这些虚函数被子类重载
    void log(const details::log_msg &msg) final;
    void flush() final;
    void set_pattern(const std::string &pattern) final;
    void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) final;

protected:
    // sink formatter
    std::unique_ptr<spdlog::formatter> formatter_;
    Mutex mutex_;

    virtual void sink_it_(const details::log_msg &msg) = 0;
    virtual void flush_() = 0;
    virtual void set_pattern_(const std::string &pattern);
    virtual void set_formatter_(std::unique_ptr<spdlog::formatter> sink_formatter);
};

继承来的4个虚函数调用protected里面的4个函数,调用前都用std::lock_guard<Mutex> lock(mutex_)加了锁。sink_it_函数负责将msg格式化成字符串buffer以及输出;flush_将缓冲区的内容写入输出。

其它的xxx_sink要么继承sink,要么继承base_sink。

4.3. basic_file_sink

以basic_file_sink为例子1,该类继承自base_sink<Mutex>

template<typename Mutex>
class basic_file_sink final : public base_sink<Mutex>
{
public:
    explicit basic_file_sink(const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {});
    const filename_t &filename() const;

protected:
    void sink_it_(const details::log_msg &msg) override;
    void flush_() override;

private:
    details::file_helper file_helper_;
};

上面sink_it_函数负责格式化msg(调用base_sink<Mutex>formatter_成员的接口format)以及将格式化后的结果输出到文件。

file_event_handlers如下。用于定义打开和关闭文件所调用的操作。

struct file_event_handlers
{
    std::function<void(const filename_t &filename)> before_open;
    std::function<void(const filename_t &filename, std::FILE *file_stream)> after_open;
    std::function<void(const filename_t &filename, std::FILE *file_stream)> before_close;
    std::function<void(const filename_t &filename)> after_close;
    file_event_handlers()
        : before_open{nullptr}
        , after_open{nullptr}
        , before_close{nullptr}
        , after_close{nullptr}
    {}
};

details::file_helper用来完成一些文件打开、关闭、写入、flush等操作。同时联动file_event_handlers,如下。除了默认构造函数,还提供了个接受file_event_handlers的构造函数。析构调用close().

SPDLOG_INLINE void file_helper::close()
{
    if (fd_ != nullptr)
    {
        if (event_handlers_.before_close)
        {
            event_handlers_.before_close(filename_, fd_);
        }

        std::fclose(fd_);
        fd_ = nullptr;

        if (event_handlers_.after_close)
        {
            event_handlers_.after_close(filename_);
        }
    }
}

对于单线程和多线程的basic_file_sink,定义了2个类型:

using basic_file_sink_mt = basic_file_sink<std::mutex>;
using basic_file_sink_st = basic_file_sink<details::null_mutex>;

其中,details::null_mutex如下,就是把std::lock_guard<Mutex> lock(mutex_)中**需要的操作都变为“空”**即可,有点巧妙。在文件include\spdlog\details\null_mutex.h中。

struct null_mutex
{
    void lock() const {}
    void unlock() const {}
    bool try_lock() const
    {
        return true;
    }
};

basic_file_sink.h中暴露出了下面2个函数,分别构造默认的多/单线程logger。

template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> basic_logger_mt(
    const std::string &logger_name, const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {})

template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> basic_logger_st(
    const std::string &logger_name, const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {})

具体是调用synchronous_factory的create函数实现。函数信息如下。创建logger同时会向注册中心(details::registry)注册该logger。

template<typename Sink, typename... SinkArgs>
static std::shared_ptr<spdlog::logger> create(std::string logger_name, SinkArgs &&... args)

4.4. rotating_file_sink

以rotating_file_sink为例子2。Rotating file即当一个日志文件达到指定大小后,就创建一个新的日志文件,以防止一个日志文件过大。

template<typename Mutex>
class rotating_file_sink final : public base_sink<Mutex>
{
public:
    rotating_file_sink(filename_t base_filename, std::size_t max_size, std::size_t max_files, bool rotate_on_open = false,
        const file_event_handlers &event_handlers = {});
    static filename_t calc_filename(const filename_t &filename, std::size_t index);
    filename_t filename();

protected:
    void sink_it_(const details::log_msg &msg) override;
    void flush_() override;

private:
    // Rotate files:
    // log.txt -> log.1.txt
    // log.1.txt -> log.2.txt
    // log.2.txt -> log.3.txt
    // log.3.txt -> delete
    void rotate_();

    // delete the target if exists, and rename the src file  to target
    // return true on success, false otherwise.
    bool rename_file_(const filename_t &src_filename, const filename_t &target_filename);

    filename_t base_filename_;
    std::size_t max_size_;
    std::size_t max_files_;
    std::size_t current_size_;
    details::file_helper file_helper_;
};

可以看到,总体上和basic_file_sink差不多,都有sink_it_flush_函数以及file_helper_。不同的是多了些进行rotating需要的函数和属性。

这部分函数的注释本身写得就比较清楚,这里我们重点关注rotate_。通过该函数的实现,结合本身提供的注释可以发现,rotating的实现是通过重命名的方式将已经存在的文件编号+1,最早创建的文件(编号最大的)删除。整个过程中file_helper_的内容始终是关于index=0的文件(即base_filename)。rotate_之后index=0的文件会被清空(原先index=0的文件变为index=1)。

sink_it_主要的功能仍然是将msg进行格式化并写入文件。与base_file_sink不同的是,如果写入后的文件大小超过了预定值,则会先rotate_再写入。

base_file_sink一样,rotating_file_sink也暴露出了构造默认的多/单线程logger的函数,长得和base_file_sink的差不多,这里就不分析了。

从上面的例子可以看出,各种sink的核心函数还是sink_it_flush_。对于不同的sink,还会有辅助该sink的一些函数。目录include\spdlog\sinks下有其它的sink,感兴趣可以看看。

五、普通logger

从第二节example中分析的大致过程来看,sink看完了就可以看看logger。看起来这个类就很重要。相关内容在include\spdlog\logger.hinclude\spdlog\logger-inl.h

5.1. logger

属性如下:

protected:
    std::string name_;
    std::vector<sink_ptr> sinks_;
    spdlog::level_t level_{level::info};
    spdlog::level_t flush_level_{level::off};
    err_handler custom_err_handler_{nullptr};
    details::backtracer tracer_;

name_sinks_level_flush_level_比较好理解,表示logger的名字、含有的sink、logger级别、进行flush操作的级别。默认logger级别为info。custom_err_handler_根据名字推测是用户自定义的错误处理方法之类的。tracer_的作用可以根据details::backtracer类提供的注释和example.cpp中的代码得到,作用是把若干条log_msg存到一个循环buffer中(具体是循环队列)。需要时再将buffer里面的log_msg输出。

下面看函数。

映入眼帘的就是一堆构造函数和一堆接受各种各样参数的log函数。

构造函数这就不说了,我们重点放在log函数中。

这些log函数互相调用,形成调用链,参数少的添加默认值后调用参数多的。根据传入log的参数,可以把调用链分成2大类:

  1. 参数中不含有格式化字符串(即"aa{}aa"这种)且要输出的内容只含有string_view_t类型(我的理解是字符串字面值):这一类的log函数最终会调用下面两个函数:

    //如果有time_point传入,则调用第一个。
    
    void log(log_clock::time_point log_time, source_loc loc, level::level_enum lvl, string_view_t msg)
    
    void log(source_loc loc, level::level_enum lvl, string_view_t msg)
    

    在这两个函数中会构造details::log_msg并调用log_it_进行输出。

  2. 其它。即含有格式化字符串要输出的内容是其它类型(如int):这类函数最终会调用下面的log_

    template<typename... Args>
    void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&...args)
    

    log_中会格式化字符串、构造details::log_msg并调用log_it_进行输出。

因此,所有的log函数最终都会构造details::log_msg并调用log_it_。下面我们来看看log_it_到底是啥。

SPDLOG_INLINE void logger::log_it_(const spdlog::details::log_msg &log_msg, bool log_enabled, bool traceback_enabled)
{
    if (log_enabled)
    {
        sink_it_(log_msg);
    }
    if (traceback_enabled)
    {
        tracer_.push_back(log_msg);
    }
}

可以看出,如果level符合输出要求,则调用logger的sink_it_进行输出。如果使用tracer的话,还会将该log_msg缓存进tracer。

因此再看看sink_it_:

SPDLOG_INLINE void logger::sink_it_(const details::log_msg &msg)
{
    for (auto &sink : sinks_)
    {
        if (sink->should_log(msg.level))
        {
            SPDLOG_TRY
            {
                sink->log(msg);
            }
            SPDLOG_LOGGER_CATCH(msg.source)
        }
    }

    if (should_flush_(msg))
    {
        flush_();
    }
}

可以看出,核心内容为检查每个sink的level是否达到输出要求,并让符合要求的sink调用它的log函数进行输出。sink的相关函数在第四节进行过分析。

logger和sink都有should_log函数。从should_log的代码可以看出,一条msg要想被输出,其level必需大于等于logger的level以及sink的level

至此,logger的核心部分就介绍完了!

5.2. backtracer

讲完了核心部分,下面就看看logger中的details::backtracer类型的成员tracer_吧。相关文件在include\spdlog\details\backtracer.h以及include\spdlog\details\backtracer-inl.h

根据前面提到的,backtracer是用来存储若干条log_msg,并在需要时一次性将存储的log_msg输出。知道这一点后再度源码应该会更容易理解。类的属性如下:

    mutable std::mutex mutex_;
    std::atomic<bool> enabled_{false};
    circular_q<log_msg_buffer> messages_;

其中,mutex_用于多线程互斥;enabled_判断该tracer是否开启;messages_为tracer中存储的所有信息。

messages_的类型中,circular_q为循环队列;log_msg_buffer为tracer存储的log_msg信息,包括logger_name和payload。

为什么不直接存log_msg类,而要弄一个log_msg_buffer呢?根据log_msg_buffer的注释和实现,我个人推测是因为log_msg中的logger_name和payload是string_view_t类型,是在栈区,而这里需要把它们放到堆区,所以搞了个log_msg_buffer

个人认为,比较核心的成员函数为enable(size_t size)disable

  • enable(size_t size)

    将属性enabled_改为true,并设置messages_最多存储size条数据。

  • disable

    将属性enabled_改为false

5.3. err_handler

接着看看logger的最后一个成员属性:custom_err_handler_,类型为err_handler,定义在include\spdlog\common.h

using err_handler = std::function<void(const std::string &err_msg)>;

其实custom_err_handler_就是一个函数,用于对错误消息进行处理,在err_handler_成员函数中被调用:

SPDLOG_INLINE void logger::err_handler_(const std::string &msg)
{
    if (custom_err_handler_)
    {
        custom_err_handler_(msg);
    }
    else
    {
        //默认err_handler
    }
}

err_handler_成员函数在对logger进行try-catch时被调用,是由宏SPDLOG_LOGGER_CATCH(location)负责。

至此,我们便介绍完了logger的部分。

六、registry

还记得我们的目标吗:看懂默认的输出(以info为例)。这是最后一步啦!

6.1. registry

registry是用来管理所有的logger的。首先浏览一下成员属性和成员函数。registry的成员属性较多,因此等用到的时候再介绍。注意到它的构造函数是私有的,并且有个static registry &instance();函数,可以判断这玩意是个单例

看看默认构造函数:

SPDLOG_INLINE registry::registry()
    : formatter_(new pattern_formatter())
{

#ifndef SPDLOG_DISABLE_DEFAULT_LOGGER
    // create default logger (ansicolor_stdout_sink_mt or wincolor_stdout_sink_mt in windows).
#    ifdef _WIN32
    auto color_sink = std::make_shared<sinks::wincolor_stdout_sink_mt>();
#    else
    auto color_sink = std::make_shared<sinks::ansicolor_stdout_sink_mt>();
#    endif

    const char *default_logger_name = "";
    default_logger_ = std::make_shared<spdlog::logger>(default_logger_name, std::move(color_sink));
    loggers_[default_logger_name] = default_logger_;

#endif // SPDLOG_DISABLE_DEFAULT_LOGGER
}

默认的sink为带颜色的sink,根据平台的不同,在wincolor_stdout_sink_mt和ansicolor_stdout_sink_mt中选择。默认的logger名字为空字符串,并将该logger保存到成员default_logger_,且由成员loggers_记录。loggers_是一个哈希表,保存由logger_name到logger的映射

继续看成员函数。

  • void register_logger(std::shared_ptr<logger> new_logger)

注册一个新logger,也即添加一个logger。如果logger名字已存在则抛出异常。

  • void initialize_logger(std::shared_ptr<logger> new_logger)

根据registry自身的成员变量设置传入logger的属性,如formatter、error_handler、level等。如果开启自动注册,则会将new_logger添加进loggers_当中。

  • void set_default_logger(std::shared_ptr<logger> new_default_logger)

顾名思义,如果不想要它默认的logger的话,就手动设置默认logger。

  • logger *get_default_raw()

default_logger_成员属性是一个智能指针。这个函数的作用是获取其原始指针(raw ptr)。这个函数是用于spdlog的默认api,如spdlog::info。它可以让默认api更快。但是**默认api不能与set_default_logger成员函数在不同线程同时使用,**否则logger就会串了。

  • void flush_every(std::chrono::seconds interval)

每隔一定周期就调用flush_all成员函数(由于flush_all较简单,这里就不介绍),让所有logger都flush一遍。最后再给成员periodic_flusher_赋值。periodic_flusher_是一个periodic_worker类的实例。periodic_worker根据名字看,是负责以一定周期实行某个任务,我们在后面会介绍它。

  • void apply_all(const std::function<void(const std::shared_ptr<logger>)> &fun)

将传入的fun运用在所有logger上。

  • void set_levels(log_levels levels, level::level_enum *global_level)

log_levels 定义为using log_levels = std::unordered_map<std::string, level::level_enum>,是logger_name到其level的映射。该函数给registry中的所有logger设置level,并更新成员属性log_levels_。如果logger_name出现在log_levels 中,则以log_levels 中对应的level为准;若没出现且global_level不为nullptr,则以global_level为准;若都不满足,则该logger的level不变。

如果global_level不为空,还会更新registry的成员global_log_level_

至此,registry中个人认为比较重要的成员函数就介绍完了。其余的都是些成员属性的getter和setter,都是先加锁后操作,代码比较简单。值得注意的是,对于线程池tp_,加锁是用的是std::recursive_mutex,其余都是std::mutexstd::recursive_mutex允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。

6.2. periodic_worker

主要部分完成后,来看看前面提到的periodic_worker类吧。正如前面提到的,periodic_worker负责以一定周期实行某个任务。看看它是怎么实现的。

成员变量:

    bool active_;                // 该worker是否工作
    std::thread worker_thread_;  // 工作线程
    std::mutex mutex_;           // 互斥量
    std::condition_variable cv_; // 同步量

在成员函数方面,就只有一个构造和一个析构。

  • 构造函数**periodic_worker(const std::function<void()> &callback_fun, std::chrono::seconds interval)**

callback_fun为要执行的内容,interval为执行的周期。函数实现如下:

SPDLOG_INLINE periodic_worker::periodic_worker(const std::function<void()> &callback_fun, std::chrono::seconds interval)
{
    active_ = (interval > std::chrono::seconds::zero());
    if (!active_)
    {
        return;
    }
	
    //线程在构建完毕后就会开始运行
    worker_thread_ = std::thread([this, callback_fun, interval]() {
        for (;;)
        {
            std::unique_lock<std::mutex> lock(this->mutex_);
            //wait_for(锁,时间周期,pred谓词判断)
            if (this->cv_.wait_for(lock, interval, [this] { return !this->active_; }))
            {
                return; // active_ == false, so exit this thread
            }
            callback_fun();
        }
    });
}

直接看重点的worker_thread_部分。该部分使用一个lambda表达式创建一个新的后台线程。该新线程会在一个死循环中等待interval的时间,如果期间active_变为false,则退出循环,结束线程;否则,调用回调函数 callback_fun执行任务。至于active_如何变为false,就要看析构函数了。涉及到cv时,锁要用unique_lock,不能用lock_guard,以便让解锁由cv管理,而不是自动管理。

  • 析构函数**~periodic_worker()**
// stop the worker thread and join it
SPDLOG_INLINE periodic_worker::~periodic_worker()
{
    if (worker_thread_.joinable())
    {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            active_ = false;
        }
        cv_.notify_one(); //这个是主线程调用的
        worker_thread_.join();
    }
}

先将active_改为false,而后通过条件变量 cv_ 发送通知,唤醒正在等待的worker_thread_,此时由于active_为false,则构造函数中cv_.wait_for会返回true,从而使worker_thread_返回。

调用worker_thread_.join()是为了等待worker_thread_结束。这样,当periodic_worker对象被销毁时,会停止worker_thread_,并等待其结束,确保资源被正确释放。

6.3. 小总结

至此,registry中我想讲的部分就结束啦!现在,我们已经有能力描述spdlog::info到底干了什么:

  1. 调用registry::instance(),初始化registry以及default_logger;
  2. 获取default_logger的原始指针,调用其info函数;
  3. 进入logger类内部,调用log函数,log函数会经过调用链,最终构造details::log_msg并调用log_it_函数;
  4. log_it_中调用sink_it_进行输出。如果使用tracer的话,还会将log_msg缓存进tracer;
  5. sink_it_函数遍历logger的所有sink,调用sink的log函数;
  6. sink的log函数是在base_sink中实现,该函数加锁并调用sink的sink_it_函数;
  7. sink_it_函数会因为sink的不同而不同,但主要功能是格式化msg以及输出格式化后的结果。

其中,一条msg要想被输出,其level必需大于等于logger的level以及sink的level

以上便是spdlog::info的流程,有没有一种苦尽甘来,收获颇丰的感觉哈哈哈哈!

下面是我在第一节类图的基础上修改后的类图。注意:出于好看和偷懒的考虑,类图中并没有展示出全部的成员函数,参数的类型也被忽略。
在这里插入图片描述

下半部分博客链接:
https://blog.csdn.net/weixin_51063895/article/details/132392927

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值