spdlog一个非常好用的C++日志库(四): 源码分析之logger类

目录

1.简介

2.类图关系

3.logger数据成员

4.logger函数成员

4.1.构造与析构

4.1.1.构造函数

4.1.2.拷贝构造、移动构造

4.2.交换操作

4.3.log()记录日志消息

4.3.1.格式串

4.3.2.普通字符串

4.3.3.日志级别

4.3.4.宽字符支持

4.4.sink_it_:将log消息交给sink对象

4.5.写日志控制

5.线程安全

5.1.backtracer类

6.错误处理

7.logger类应用

7.1.创建logger对象

7.1.1.同步工厂方法synchronous_factory

7.1.2.异步工厂方法

7.2.获取logger对象

7.3.使用logger对象

7.4.删除logger对象

8.async_logger类

8.1.async_logger数据成员

8.2.async_logger构造与析构

8.3.async_logger的clone

8.4.async_logger前端接收log消息

8.5.async_logger后端写log消息


1.简介

一个logger类对象代表一个日志记录器,为用户提供日志记录接口。包括哪些功能?
基本功能:

  • logger名称,用于唯一标识该logger
  • 日志等级
  • 接收用户日志消息的接口
  • 提供一个sink(目标文件)指针数组和formatter(格式化),用于转换格式串并写到目标文件
  • 线程安全
  • 错误处理

高级功能:

  • 环形队列缓存最近消息,便于回溯
  • 自定义错误处理

2.类图关系

与logger有关的类图关系示意图:

log_msg 包含了logger名称、日志等级、记录log时间点、调用处信息,以及负责用户log消息等等,是一条log消息的原始组成部分;
source_loc 包含调用处的文件名、函数名、行数信息;

synchronous_factory 同步工厂,并非logger成员,用于创建非线程安全版本的logger对象;
async_factory,async_factory_nonblock,异步工厂,有2个版本,决定了当线程池缓冲区满时的策略,是阻塞等待 or 丢弃最老的,用于创建线程安全版本的logger对象;

sink 负责将log_msg转换为最终的log字符串,然后写入指定的目标文件。

3.logger数据成员

每个logger都拥有一个名字,全局注册表registry使用logger name来区分不同的logger对象,因此每个logger name应该不同。
一个logger对象包含多个sink对象,是因为用户可能需要一份log消息写到多个目标文件上,而一个sink对象代表了一个输出文件。

为何会有2个level_t日志等级成员(level_, flush_level_)?
设置2个level_t类型日志等级成员,是为了更精细化控制log消息。
当log消息log_msg的日志等级 > level_时,允许log消息写到目标文件(sink);
当log消息log_msg的日志等级 > flush_level_时,允许log消息flush(冲刷)到目标文件(sink);

当记录日志时,spdlog不会抛出异常。但构造logger或sink对象时,可能发生异常,这被认为是致命的。如果一个错误发生在记录日志时,默认情况下,库将打印一个错误信息到stderr。而custom_err_handler_是便于用户修改默认的错误处理。

tracer_ 用一个环形队列,记录最近的几个log message,当用户想要回溯时,tracer_ 便是一个好的选择。

protected:
    std::string name_;                          // logger名字
    std::vector<sink_ptr> sinks_;               // logger接收器(多个)
    spdlog::level_t level_{level::info};        // 记录日志等级, 决定是否允许log log_msg
    spdlog::level_t flush_level_{level::off};   // flush日志等级, 决定是否允许flush log_msg
    err_handler custom_err_handler_{nullptr};   // 用户自定义错误处理回调
    details::backtracer tracer_;                // 回溯最近的一些log message, 使用环形队列存储             // 回溯最近的一些log message, 使用环形队列存储

这些成员设为protected,是允许派生类直接访问。

4.logger函数成员

4.1.构造与析构

4.1.1.构造函数

根据是否传入sink对象,构造函数分为两类:
1)空sink对象;
2)由调用者传入若干sink对象;

第2)种情况,sink对象有多种形式:单个sink对象、迭代器表示的范围、初始化列表。

public:
    // Empty logger
    explicit logger(std::string name)
        : name_(std::move(name))
        , sinks_()
    {}

    // Logger with range on sinks
    template<typename It>
    logger(std::string name, It begin, It end)
        : name_(std::move(name))
        , sinks_(begin, end)
    {}

    // Logger with single sink
    logger(std::string name, sink_ptr single_sink)
        : logger(std::move(name), {std::move(single_sink)})
    {}

    // Logger with sinks init list
    logger(std::string name, sinks_init_list sinks)
        : logger(std::move(name), sinks.begin(), sinks.end())
    {}

    virtual ~logger() = default;

    logger(const logger &other);
    logger(logger &&other) SPDLOG_NOEXCEPT;
    logger &operator=(logger other) SPDLOG_NOEXCEPT;

析构函数使用default(编译器自动合成的),virtual析构函数意味着该类可能会被继承。

4.1.2.拷贝构造、移动构造

// public methods
// copy ctor
SPDLOG_INLINE logger::logger(const logger &other)
    : name_(other.name_)
    , sinks_(other.sinks_)
    , level_(other.level_.load(std::memory_order_relaxed))
    , flush_level_(other.flush_level_.load(std::memory_order_relaxed))
    , custom_err_handler_(other.custom_err_handler_)
    , tracer_(other.tracer_)
{}

// move ctor
SPDLOG_INLINE logger::logger(logger &&other) SPDLOG_NOEXCEPT
    : name_(std::move(other.name_))
    , sinks_(std::move(other.sinks_))
    , level_(other.level_.load(std::memory_order_relaxed))
    , flush_level_(other.flush_level_.load(std::memory_order_relaxed))
    , custom_err_handler_(std::move(other.custom_err_handler_))
    , tracer_(std::move(other.tracer_))
{}

// operator=
SPDLOG_INLINE logger &logger::operator=(logger other) SPDLOG_NOEXCEPT
{
    this->swap(other);
    return *this;
}

什么时候使用直接构造,什么时候使用移动构造(std::move)?
移动操作的目的是避免即将释放的对象重复构造,也就是说,如果一个对象即将释放,用它来构造另一个对象的行为就可以改成移动构造。

4.2.交换操作

交换操作并没有使用通用的std::swap,因为通用的swap会构造一个新的临时对象,然后再赋值。因此,通用的swap操作是最后选择。

对于基本类型,swap操作是直接赋值;
对于对象类型,优先调用对象的swap成员函数,最后才是调用通用swap操作;
对于原子类型,使用专用的赋值或者交换函数;

// swap成员函数
SPDLOG_INLINE void logger::swap(spdlog::logger &other) SPDLOG_NOEXCEPT
{
    name_.swap(other.name_);
    sinks_.swap(other.sinks_);

    // swap level_
    auto other_level = other.level_.load();
    auto my_level = level_.exchange(other_level);
    other.level_.store(my_level);

    // swap flush level_
    other_level = other.flush_level_.load();
    my_level = flush_level_.exchange(other_level);
    other.flush_level_.store(my_level);

    custom_err_handler_.swap(other.custom_err_handler_);
    std::swap(tracer_, other.tracer_);
}

// 重载swap函数
SPDLOG_INLINE void swap(logger &a, logger &b)
{
    a.swap(b);
}

4.3.log()记录日志消息

记录日志消息操作的目的是接受用户输入的log消息,构造一个log_msg对象,然后交给所拥有的每个sink对象,从而将log消息写到目标文件上。

4.3.1.格式串

由于需要支持参数不定的格式字符串,spdlog使用变长模板来支持这一特性。

    // 参数完整的记录日志接口
    // 用户输入的是变长参数args
    template<typename... Args>
    void log(source_loc loc, level::level_enum lvl, format_string_t<Args...> fmt,  Args &&... args)
    {
        log_(loc, lvl, fmt, std::forward<Args>(args)...); // 转发给private接口log_
    }

为了简化接口,spdlog使用一组参数使用了默认值的log的重载函数,为用户提供记录日志接口。它们都调用了参数完整版的log<...>()

    template<typename... Args>
    void log(level::level_enum lvl, format_string_t<Args...> fmt, Args &&... args)
    {
        log(source_loc{}, lvl, fmt, std::forward<Args>(args)...); // source_loc为空
    }

    template<typename T>
    void log(level::level_enum lvl, const T &msg)
    {
        log(source_loc{}, lvl, msg); // source_loc为空, T类型能转换为格式串
    }

    // T cannot be statically converted to format string (including  string_view/wstring_view)
    template<class T, typename  std::enable_if<!is_convertible_to_any_format_string<const T &>::value, int>::type  = 0>
    void log(source_loc loc, level::level_enum lvl, const T &msg)
    {
        log(loc, lvl, "{}", msg); // source_loc为空, T类型不能转换为格式串, 直接将其转换为字符串
    }

可以看出,变长参数的log其实是交给log_的来实现的,而

    // common implementation for after templated public api has been resolved
    template<typename... Args>
    void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&...  args)
    {
        bool log_enabled = should_log(lvl);         // 只有优先级不低于指定优先级的log消息, 才被允许记录
        bool traceback_enabled = tracer_.enabled(); // 是否允许回溯最近的log消息
        if (!log_enabled && !traceback_enabled)
        {
            return;
        }
        SPDLOG_TRY
        {
            memory_buf_t buf; // 二进制缓存
#ifdef SPDLOG_USE_STD_FORMAT
            fmt_lib::vformat_to(std::back_inserter(buf), fmt,  fmt_lib::make_format_args(std::forward<Args>(args)...));
#else
            // seems that fmt::detail::vformat_to(buf, ...) is ~20ns faster than  fmt::vformat_to(std::back_inserter(buf),..)
            fmt::detail::vformat_to(buf, fmt,  fmt::make_format_args(std::forward<Args>(args)...));
#endif
            details::log_msg log_msg(loc, name_, lvl, string_view_t(buf.data(),  buf.size()));
            log_it_(log_msg, log_enabled, traceback_enabled);
        }
        SPDLOG_LOGGER_CATCH(loc)
    }

// protected methods
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); // 将log_msg交给sink
    }
    if (traceback_enabled)
    {
        tracer_.push_back(log_msg); // 环形队列缓存log_msg
    }
}

从log_实现上,可以看出,变长模板的处理,最终是交给ftm库的vformat_to函数处理了。

4.3.2.普通字符串

上面是处理的格式串,如果普通字符串也这样处理,效率会很低。logger类提供了更高效的方法。

    // 用户输入的是普通字符串string_view_t
    void log(source_loc loc, level::level_enum lvl, string_view_t msg)
    {
        bool log_enabled = should_log(lvl);
        bool traceback_enabled = tracer_.enabled();
        if (!log_enabled && !traceback_enabled)
        {
            return;
        }

        details::log_msg log_msg(loc, name_, lvl, msg);
        log_it_(log_msg, log_enabled, traceback_enabled);
    }

    // 简化版, 调用者无需指定source_loc
    void log(level::level_enum lvl, string_view_t msg)
    {
        log(source_loc{}, lvl, msg);
    }

如果用户想要指定log时间,logger也提供了对应接口

    // 调用者可以指定log时间点
    void log(log_clock::time_point log_time, source_loc loc, level::level_enum  lvl, string_view_t msg)
    {
        bool log_enabled = should_log(lvl);         // 使能log level
        bool traceback_enabled = tracer_.enabled(); // 使能回溯
        if (!log_enabled && !traceback_enabled)
        {
            return;
        }
        details::log_msg log_msg(log_time, loc, name_, lvl, msg);
        log_it_(log_msg, log_enabled, traceback_enabled);
    }

4.3.3.日志级别

如果用户不想每次写log,都带上一个log level参数,该怎么办?
logger针对所有log level提供了一组写log消息的接口。

    // 针对各种日志级别的写log接口
    template<typename... Args>
    void trace(format_string_t<Args...> fmt, Args &&... args)
    {
        log(level::trace, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void debug(format_string_t<Args...> fmt, Args &&... args)
    {
        log(level::debug, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void info(format_string_t<Args...> fmt, Args &&... args)
    {
        log(level::info, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void warn(format_string_t<Args...> fmt, Args &&... args)
    {
        log(level::warn, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void error(format_string_t<Args...> fmt, Args &&... args)
    {
        log(level::err, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void critical(format_string_t<Args...> fmt, Args &&... args)
    {
        log(level::critical, fmt, std::forward<Args>(args)...);
    }

针对不同日志级别的极简版写log接口,支持log消息类型为模板参数T,但要求能转换为string_view_t。这类接口特点是只需用户提供log消息,而且无需是string_view_t,只需要能隐式转换即可;选择调用的接口本身,就代表了日志级别。

    template<typename T>
    void trace(const T &msg)
    {
        log(level::trace, msg); // msg必须能转换为string_view_t类型, 否则编译报错
    }

    template<typename T>
    void debug(const T &msg)
    {
        log(level::debug, msg);
    }

    template<typename T>
    void info(const T &msg)
    {
        log(level::info, msg);
    }

    template<typename T>
    void warn(const T &msg)
    {
        log(level::warn, msg);
    }

    template<typename T>
    void error(const T &msg)
    {
        log(level::err, msg);
    }

    template<typename T>
    void critical(const T &msg)
    {
        log(level::critical, msg);
    }

4.3.4.宽字符支持

Windows下,可能使用宽字符问题,通过宏定义SPDLOG_WCHAR_TO_UTF8_SUPPORT来控制。logger提供了对应接口,跟非宽字符版本区别是:将format_string_t替换为wformat_string_t,将string_view_t替换为wstring_view_t。

例如,

    template<typename... Args>
    void log(source_loc loc, level::level_enum lvl, wformat_string_t<Args...> fmt,  Args &&... args)
    {
        log_(loc, lvl, fmt, std::forward<Args>(args)...);
    }

宽字符版本,最终也是通过非宽字符版本实现的,中间多了个一个宽字符串到非宽字符串的转换:

    // 注意与非宽字符版本区别:msg参数类型为wstring_view_t, 实现上多了宽字符转换
    void log(log_clock::time_point log_time, source_loc loc, level::level_enum  lvl, wstring_view_t msg)
    {
        bool log_enabled = should_log(lvl);
        bool traceback_enabled = tracer_.enabled();
        if (!log_enabled && !traceback_enabled)
        {
            return;
        }

        memory_buf_t buf;
        details::os::wstr_to_utf8buf(wstring_view_t(msg.data(), msg.size()), buf); // 将宽字符转换为utf8字符
        details::log_msg log_msg(log_time, loc, name_, lvl,  string_view_t(buf.data(), buf.size()));
        log_it_(log_msg, log_enabled, traceback_enabled);
    }

4.4.sink_it_:将log消息交给sink对象

前面log_it_提到,log level使能(优先级符合要求)时,将构造的log消息对象交给sink对象。

// protected methods
SPDLOG_INLINE void logger::sink_it_(const details::log_msg &msg)
{
    for (auto &sink : sinks_) // logger拥有一个sink列表, 每个sink对象都有机会得到该log_msg对象
    {
        if (sink->should_log(msg.level))
        {
            SPDLOG_TRY
            {
                sink->log(msg);
            }
            SPDLOG_LOGGER_CATCH(msg.source)
        }
    }

    if (should_flush_(msg)) // 根据flush_level_判断是否允许flush
    {
        flush_();
   }
}

4.5.写日志控制

有2个控制接口:
should_log,控制是否允许写用户传入的log消息,采用策略是log消息本身级别(用户指定) >= logger指定的日志级别(创建者指定);
should_backtrace,控制是否允许回溯log消息,回溯策略是开启了该功能时,在写log消息同时,会将log消息加入到回溯用的环形队列tracer_中。

    // return true logging is enabled for the given level.
    bool should_log(level::level_enum msg_level) const
    {
        return msg_level >= level_.load(std::memory_order_relaxed);
    }

    // return true if backtrace logging is enabled.
    bool should_backtrace() const
    {
        return tracer_.enabled();
    }

还有一个私有的控制接口should_flush_,用来控制是否允许冲刷log消息。其策略类似于shoud_log。

SPDLOG_INLINE bool logger::should_flush_(const details::log_msg &msg)
{
    auto flush_level = flush_level_.load(std::memory_order_relaxed);
    return (msg.level >= flush_level) && (msg.level != level::off);
}

5.线程安全

严格来说,logger类本身并不提供线程安全保证,其线程安全是通过数据成员实现的。

name_通常构造时决定,后续不修改,但程序并未提供这种保证,因为是non-const。
level_和flush_level_都是level_t类型(即原子类型),无需考虑线程安全,需要考虑原子操作顺序,即内存布局。logger中,这2个原子变量内存布局都是松散的(std::memory_order_relaxed)。

#if defined(SPDLOG_NO_ATOMIC_LEVELS)
using level_t = details::null_atomic_int;
#else
using level_t = std::atomic<int>;
#endif

custom_err_handler_类型是std::function<void(const std::string &err_msg)>,并不提供线程安全保证,但又提供了set接口(set_error_handler),因此,该成员的访问不是线程安全的。

sinks_ 是std::vector<sink_ptr>,其线程安全依赖于sink类。sink类是一个抽象类,其线程安全依赖于派生类。spdlog中sink派生类,通过模板参数Mutex来决定锁类型,这为一套代码实现两套方案:无锁(_st)和有锁(_mt)提供支持。会提供专门的一文来讲解sink类。
tracer_是backtracer类型,其线程安全依赖于backtracer类。

5.1.backtracer类

backtracer类通过一个固定大小的环形队列messages_缓存最近log消息,为logger实现回溯log消息。向backtracer插入(push_back)前,必须通过enable()指定环形队列大小,否则环形队列messages_大小为0,无法插入数据。

class SPDLOG_API backtracer
{
    mutable std::mutex mutex_;            // 互斥锁
    std::atomic<bool> enabled_{false};    // backtracer使能状态
    circular_q<log_msg_buffer> messages_; // 环形队列

public:
    backtracer() = default; // default ctor
    backtracer(const backtracer &other); // copy ctor

    backtracer(backtracer &&other) SPDLOG_NOEXCEPT; // move ctor
    backtracer &operator=(backtracer other); // operator=

    void enable(size_t size); // 使能backtracer功能, 为环形队列指定大小
    void disable();           // 禁用backtracer, 但不会清除环形队列大小
    bool enabled() const;     // 返回backtracer使能状态
    void push_back(const log_msg &msg); // 向环形队列末尾插入一条log消息

    // pop all items in the q and apply the given fun on each of them.
    void foreach_pop(std::function<void(const details::log_msg &)> fun);
};

backtracer使用环形队列有2个比较重要的操作:push_back,向环形队列尾部插入一条log消息。当队列满时,并没有用阻塞等待的策略,而是用的默认的丢弃最老的log消息;
foreach_pop,逐条从环形队列头弹出log消息,并对每个弹出的log消息应用指定的fun函数。通过这种方式,让用户有机会对环形队列中的log消息进行处理。

foreach_pop代码如下:

// pop all items in the q and apply the given fun on each of them.
SPDLOG_INLINE void backtracer::foreach_pop(std::function<void(const  details::log_msg &)> fun)
{
    std::lock_guard<std::mutex> lock{mutex_};
    // 从队列messages_ 头逐个弹出log消息,并作为fun参数进行调用
    while (!messages_.empty())
    {
        auto &front_msg = messages_.front();
        fun(front_msg);
        messages_.pop_front();
    }
}

logger的转储dump_backtrace_()功能,就是用到了backtracer::foreach_pop,将环形队列中每条log消息都交给sink写到目标文件。该功能对于排查问题时,查看最近的log消息十分有用。

SPDLOG_INLINE void logger::dump_backtrace_()
{
    using details::log_msg;
    if (tracer_.enabled())
    {
        sink_it_(log_msg{name(), level::info, "****************** Backtrace Start  ******************"});
        tracer_.foreach_pop([this](const log_msg &msg) { this->sink_it_(msg); });
        sink_it_(log_msg{name(), level::info, "****************** Backtrace End  ********************"});
    }
}

6.错误处理

logger类定义了错误回调函数err_handler_,也提供用户自定义错误回调custom_err_handler_。缺省的错误处理方式是,向stderr打印一条错误信息,包含错误发生时间、错误计数、logger名称、错误正文消息等。

SPDLOG_INLINE void logger::err_handler_(const std::string &msg)
{
    if (custom_err_handler_) // 自定义错误处理回调
    {
        custom_err_handler_(msg);
    }
    else // 缺省的错误处理
    {
        using std::chrono::system_clock;
        static std::mutex mutex;
        static std::chrono::system_clock::time_point last_report_time;
        static size_t err_counter = 0;  // 错误计数器
        std::lock_guard<std::mutex> lk{mutex};
        auto now = system_clock::now(); // 错误发生时间点
        err_counter++;
        if (now - last_report_time < std::chrono::seconds(1)) // 确保两次错误时间间隔不会太小,以至于错误信息充斥屏幕
        {
            return;
        }
        last_report_time = now;
        auto tm_time = details::os::localtime(system_clock::to_time_t(now));
        char date_buf[64];
        std::strftime(date_buf, sizeof(date_buf), "%Y-%m-%d %H:%M:%S", &tm_time);
#if defined(USING_R) && defined(R_R_H) // if in R environment
        REprintf("[*** LOG ERROR #%04zu ***] [%s] [%s] {%s}\n", err_counter,  date_buf, name().c_str(), msg.c_str());
#else
        std::fprintf(stderr, "[*** LOG ERROR #%04zu ***] [%s] [%s] {%s}\n",  err_counter, date_buf, name().c_str(), msg.c_str());
#endif
    }
}

什么时候可能发生错误?发生何种错误?
言外之意,就是什么时候由谁调用logger::err_handler_。logger中,err_handler_的调用实际上封装到了捕获异常的宏定义SPDLOG_LOGGER_CATCH中,

#ifndef SPDLOG_NO_EXCEPTIONS
#    define SPDLOG_LOGGER_CATCH(location)  \
        catch (const std::exception &ex)   \
        {                                  \
            if (location.filename)         \
            {                              \
                err_handler_(fmt_lib::format(SPDLOG_FMT_STRING("{} [{}({})]"),  ex.what(), location.filename, location.line));  \
            }                              \
            else                           \
            {                              \
                err_handler_(ex.what());   \
            }                              \
        }                                  \
        catch (...)                        \
        {                                  \
            err_handler_("Rethrowing unknown exception in logger");  \
            throw;                         \
        }
#else
#    define SPDLOG_LOGGER_CATCH(location)
#endif

而logger中需要用SPDLOG_LOGGER_CATCH捕获异常的,只有log_和sink_it。
对于log_,在尝试将log消息对应的格式串转换为普通string_view_t,以及利用sink写文件时(sink_it)。而调用log_()的,只有用户通过log()接口写入log消息的时候。
对于sink_it,在尝试格式化log消息,写文件的时候。

7.logger类应用

7.1.创建logger对象

在spdlog中,用户并不直接创建logger对象,而是通过工厂方法根据不同的sink,来创建logger对象。例如,下面代码用工厂方法创建一个logger对象:

// Create and return a shared_ptr to a multithread console logger.
#include "spdlog/sinks/stdout_color_sinks.h"
auto console = spdlog::stdout_color_mt("some_unique_name");

函数模板stdout_color_mt的定义是这样的:

// stdout_color_mt声明, 模板参数Factory默认使用同步工厂synchronous_factory
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> stdout_color_mt(const std::string &logger_name, color_mode  mode = color_mode::automatic);

// stdout_color_mt定义, 使用工厂方法创建logger对象
template<typename Factory>
SPDLOG_INLINE std::shared_ptr<logger> stdout_color_mt(const std::string  &logger_name, color_mode mode)
{
    return Factory::template create<sinks::stdout_color_sink_mt>(logger_name,  mode);
}

7.1.1.同步工厂方法synchronous_factory

通常,一个工厂方法创建一种对象,如果想创建不同类型的对象,就传入参数,工厂方法内部进行判断后创建不同类型对象。synchronous_factory的精妙之处在于,函数参数用来创建对象,模板参数用来指定要创建的类型(有关的部分)。

logger name对于registry全局注册表来说,是唯一标识logger对象的。

这里有一个潜在的约定,所有工厂方法必须实现一个static create方法,通过模板参数Sink创建不同类型Sink派生类对象,然后绑定到新建的logger对象,从而实现不同的功能。

// Default logger factory-  creates synchronous loggers
class logger;

struct synchronous_factory
{
    template<typename Sink, typename... SinkArgs>
    static std::shared_ptr<spdlog::logger> create(std::string logger_name,  SinkArgs &&... args)
    {
        auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...); // 模板参数Sink决定了要具体Sink类型
        auto new_logger = std::make_shared<spdlog::logger>(std::move(logger_name),  std::move(sink)); // 用logger name及sink来创建logger对象
        details::registry::instance().initialize_logger(new_logger); // 初始化logger, 并添加到全局注册表
        return new_logger;
    }
};

7.1.2.异步工厂方法

针对所使用的环形队列,当队列满时,如果插入数据,有两种策略:阻塞、非阻塞,分别对应工厂类型async_factory、async_factory_nonblock。

using async_factory = async_factory_impl<async_overflow_policy::block>;  // 阻塞策略
using async_factory_nonblock =  async_factory_impl<async_overflow_policy::overrun_oldest>;  // 非阻塞策略

可以看到上面2种工厂类型,都是通过async_factory_impl来实现的。那么,async_factory_impl是如何实现的呢?
async_factory_impl也遵循工厂方法的潜规则:提供static create方法,根据模板参数Sink创建不同类型sink对象并绑定到新建的logger对象。

// async logger factory - creates async loggers backed with thread pool.
// if a global thread pool doesn't already exist, create it with default queue
// size of 8192 items and single thread.
template<async_overflow_policy OverflowPolicy = async_overflow_policy::block>
struct async_factory_impl
{
    template<typename Sink, typename... SinkArgs>
    static std::shared_ptr<async_logger> create(std::string logger_name, SinkArgs  &&... args)
    {
        auto &registry_inst = details::registry::instance();
        
        // 如果全局线程池不存在,就创建一个
        // create global thread pool if not already exists..

        auto &mutex = registry_inst.tp_mutex();
        std::lock_guard<std::recursive_mutex> tp_lock(mutex);
        auto tp = registry_inst.get_tp();
        if (tp == nullptr)
        {
            tp =  std::make_shared<details::thread_pool>(details::default_async_q_size, 1U);
            registry_inst.set_tp(tp);
        }

        auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...);
        // 创建新async_logger对象同时, 绑定线程池
        auto new_logger = std::make_shared<async_logger>(std::move(logger_name),  std::move(sink), std::move(tp), OverflowPolicy);
        registry_inst.initialize_logger(new_logger);
        return new_logger;
    }

跟同步工厂方法最大的区别是:异步工厂方法,是依附于一个(registry单例管理的)全局线程池的。创建出来的logger对象真实类型是派生类async_logger。而async_logger通过一个弱指针指向线程池。

上面的只是工厂的类型,并非工厂方法。用户想要利用工厂方法创建对象,需要用到下面的create_async, create_async_nb方法:

// 采用阻塞策略的异步工厂方法
template<typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async(std::string logger_name,  SinkArgs &&... sink_args)
{
    return async_factory::create<Sink>(std::move(logger_name),  std::forward<SinkArgs>(sink_args)...);
}

// 采用非阻塞策略的异步工厂方法
template<typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async_nb(std::string logger_name,  SinkArgs &&... sink_args)
{
    return async_factory_nonblock::create<Sink>(std::move(logger_name),  std::forward<SinkArgs>(sink_args)...);
}

在客户端,比如你想创建一个basic_logger_mt,即一个基本都用于多线程环境的async_logger,可以这样封装工厂方法,然后供APP调用:

// include/spdlog/sinks/basic_file_sink.h

// 封装工厂方法,供APP调用
// factory functions
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 = {})
{
    return Factory::template create<sinks::basic_file_sink_mt>(logger_name,  filename, truncate, event_handlers);
}

// APP端创建async_logger对象
// spdlog::init_thread_pool(32768, 1); // queue with max 32k items 1 backing  thread.
auto async_file =  spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger",  "logs/async_log.txt");

总结一下,定义并使用工厂方法的方式:
1)定义工厂类,提供static create方法,根据模板参数决定绑定到logger对象的Sink类型,从而决定不同输出目标;
2)对于异步工厂方法,还要将线程池绑定到logger对象;
3)返回的最终都是共享指针管理的logger对象;
4)为工厂方法提供一个包装方法,指定具体的模板参数Sink类型;

7.2.获取logger对象

spdlog中,使用工厂方法创建的logger对象,会自动注册到全局注册表registry,便于查询、管理。可用spdlog::get()方法获取已注册的loggers。

例如,创建名为"some_logger"的logger对象,并用spdlog::get获取:

auto my_logger = spdlog::basic_logger_mt("some_logger"); // 使用默认的同步工厂方法
...
auto some_logger = spdlog::get("some_logger");

注意:spdlog::get获取的类型跟创建时类型一致,都是shared_ptr管理的logger对象。

7.3.使用logger对象

获取到logger对象后,就能调用对应public接口了,譬如调用trace/log等接口就可以写log消息了。

例如,下面代码往日志文件("logs/async_log.txt")写内容"Async message #a"

auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger", "logs/async_log.txt");
int a = 10;
async_file->info("Async message #{}", a);

7.4.删除logger对象

删除logger对象是registry内容,详细见讲解registry类的文章,此处简要描述下。全局注册表registry是logger对象的持有者,可调用registry::drop删除指定logger名称的logger,或者调用registry::drop_all删除所有的logger。
注意:logger对象是通过shared_ptr管理,注册到registry的map存储结构中,因此只能将其从map中删除,而不会立即释放对象,需要等到引用计数为0。

spdlog::registry::drop("some_logger"); // 删除logger名称为"some_logger"的logger对象

spdlog::registry::drop_all();          // 删除所有logger对象

8.async_logger类

async_logger类是logger类的派生类,专门用于接收用户log消息,然后交给线程池异步写入目标文件。用户提交log消息的线程,称为前端线程;将log消息写到目标文件的线程,称为后端线程。

8.1.async_logger数据成员

async_logger并非线程池的创建者,而线程池会用到logger的共享指针,而该指针可能指向async_logger对象,因此,async_logger使用thread_pool的弱指针。

在通过线程池往环形队列添加log消息时,可以指明所需的阻塞策略。async_logger给了调用者在构造时,就指定阻塞策略的机会,通过数据成员overflow_policy_记录。

private:
    std::weak_ptr<details::thread_pool> thread_pool_;
    async_overflow_policy overflow_policy_;                // 环形队列满时 阻塞策略

8.2.async_logger构造与析构

async_logger最完整的构造函数,是利用了sink迭代器范围来构造:

public:
    // begin, end是指向sink的迭代器范围
    template<typename It>
    async_logger(std::string logger_name, It begin, It end,  std::weak_ptr<details::thread_pool> tp,
        async_overflow_policy overflow_policy = async_overflow_policy::block)
        : logger(std::move(logger_name), begin, end)
        , thread_pool_(std::move(tp))
        , overflow_policy_(overflow_policy)
    {}

在此基础上,提供了2个便捷的构造接口,便于使用sink的初始化列表、单个sink对象来构造async_logger对象:

// 下面2个构造函数都是基于上面sink迭代器范围实现的
SPDLOG_INLINE spdlog::async_logger::async_logger(
    std::string logger_name, sinks_init_list sinks_list,  std::weak_ptr<details::thread_pool> tp, async_overflow_policy overflow_policy)
    : async_logger(std::move(logger_name), sinks_list.begin(), sinks_list.end(),  std::move(tp), overflow_policy)
{}

SPDLOG_INLINE spdlog::async_logger::async_logger(
    std::string logger_name, sink_ptr single_sink,  std::weak_ptr<details::thread_pool> tp, async_overflow_policy overflow_policy)
    : async_logger(std::move(logger_name), {std::move(single_sink)},  std::move(tp), overflow_policy)
{}

async_logger的析构函数就直接使用编译器默认合成的。

8.3.async_logger的clone

clone实际上是基类logger中定义的virtual函数,async_logger对象的clone类似,不过构造的对象是async_logger类型,而非logger类型。

SPDLOG_INLINE std::shared_ptr<spdlog::logger>  spdlog::async_logger::clone(std::string new_name)
{
    auto cloned = std::make_shared<spdlog::async_logger>(*this);
    cloned->name_ = std::move(new_name);
    return cloned;
}

想一想:为什么不用copy构造,来构造一个新async_logger对象?
因为clone需要的是一个除了logger name不同,其他属性均相同点async_logger对象。

8.4.async_logger前端接收log消息

async_logger的主要任务是什么?
从前端线程接收用户log消息,然后将其交给线程池;线程池空闲时,会调用async_logger来处理当前log消息,将其写到目标文件(sink)。

protected方法sink_it_就是用于前端线程,将接收到的用户log消息转交给线程池;flush_是向线程池发送一条flush异步消息,通知线程池尽早将log消息写到目标文件。

// send the log message to the thread pool
SPDLOG_INLINE void spdlog::async_logger::sink_it_(const details::log_msg &msg)
{
    if (auto pool_ptr = thread_pool_.lock())
    {
        pool_ptr->post_log(shared_from_this(), msg, overflow_policy_); // 将log消息转交给线程池
    }
    else
    {
        throw_spdlog_ex("async log: thread pool doesn't exist anymore");
    }
}

// send flush request to the thread pool
SPDLOG_INLINE void spdlog::async_logger::flush_()
{
    if (auto pool_ptr = thread_pool_.lock())
    {
        pool_ptr->post_flush(shared_from_this(), overflow_policy_); // 发送一条flush消息给线程池, 将缓存内容尽早flush到文件
    }
    else
    {
        throw_spdlog_ex("async flush: thread pool doesn't exist anymore");
    }
}

8.5.async_logger后端写log消息

backend_sink_it_和backend_flush_是运行于后端线程(线程池子线程),分别对应前端任务sink_it_和flush_。

// backend functions - called from the thread pool to do the actual job
SPDLOG_INLINE void spdlog::async_logger::backend_sink_it_(const details::log_msg  &msg)
{
    for (auto &sink : sinks_)
    {
        if (sink->should_log(msg.level))
        {
            SPDLOG_TRY
            {
                sink->log(msg); // 将log消息交给sink对象,写到目标文件
            }
            SPDLOG_LOGGER_CATCH(msg.source)
        }
    }

    if (should_flush_(msg)) // 如果允许的话,自动在后端flush
    {
        backend_flush_();
    }
}

SPDLOG_INLINE void spdlog::async_logger::backend_flush_()
{
    for (auto &sink : sinks_)
    {
        SPDLOG_TRY
        {
            sink->flush(); // 通知sink冲刷缓存到文件
        }
        SPDLOG_LOGGER_CATCH(source_loc())
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/771840.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【内网渗透】从0到1的内网渗透基础概念笔记

目录 域 域的介绍 单域 父域和子域 域树 域森林 域名服务器 活动目录 活动目录介绍 域内权限 组 域本地组 全局组 通用组 总结 示例 A-G-DL-P策略 重要的域本地组 重要的全局组、通用组 安全域划分 域 域的介绍 Windows域是计算机网络的一种形式&#xf…

币界网讯,预计以太坊现货 ETF 将于 7 月中旬推出

刚刚 ETF Store 总裁 Nate Geraci 在 X &#xff08;前Twitter&#xff09;平台上宣布&#xff0c;备受数字货币市场期待的SEC以太坊现货 ETF提案&#xff0c;将于7 月中旬通过美国证券交易委员会&#xff08;SEC&#xff09;批准。Nate Geraci透露修订后的 S-1 文件将于 7 月 …

艺活网DIY手工制作网站源码 工艺制作教程平台源码,带数据

帝国CMS仿《手艺活》DIY手工制作网源码&#xff0c;仿手艺活自适应手机版模板。 带数据库和图片资源&#xff0c;一共5个G大小&#xff0c;下载需耐心。 92开发 手艺活网DIY手工制作网站源码 创意手工艺品制作教程平台系统帝国h5自适应手机端 是一套展示各种 DIY 小物品精美又…

初学Spring之自动装配 Bean

Bean 的作用域&#xff1a; 1.单例模式&#xff08;Spring 默认机制&#xff09; scope“singleton” 2.原型模式&#xff1a;每次从容器中 get 时&#xff0c;都会产生一个新对象 scope"prototype" 3. request、session、application&#xff0c;只能在 web 开…

webp2jpg网页在线图片格式转换源码

源码介绍 webp2jpg-免费在线图片格式转化器, 可将jpeg、jpg、png、gif、 webp、svg、ico、bmp文件转化为jpeg、png、webp、webp动画、gif文件。 无需上传文件&#xff0c;本地即可完成转换! 源码特点&#xff1a; 无需上传&#xff0c;使用浏览器自身进行转换批量转换输出we…

九、函数的声明和定义

函数声明&#xff1a; 1. 告诉编译器有一个函数叫什么&#xff0c;参数是什么&#xff0c;返回类型是什么。但是具体是不是存在&#xff0c;函数 声明决定不了。 2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。 3. 函数的声明一般要放在头文件中的。 定义的函…

视频监控平台web客户端的免密查看视频页:在PC浏览器上如何调试手机上的前端网页(PC上的手机浏览器的开发者工具)

目录 一、手机上做前端页面开发调试 1、背景 2、视频监控平台AS-V1000的视频分享页 3、调试手机前端页面代码的条件 二、手机端的准备工作 1、手机准备 2、手机的开发者模式 3、PC和手机的连接 &#xff08;1&#xff09;进入调试模式 &#xff08;2&#xff09;选择…

期权开户零门槛怎么操作?期权不满50w的开户方式

今天带你了解期权开户零门槛怎么操作&#xff1f;期权不满50w的开户方式。在股票期权市场上&#xff0c;期权交易是一种非常受欢迎的投资方式。它不仅可以增加投资组合的多样性&#xff0c;还可以为投资者提供一定的保护和利润机会&#xff0c;比如通过买入认股期权做空对冲大盘…

基于Springboot的智慧信息化机房管理系统

1 项目介绍 1.1 研究目的和意义 随着社会的快速发展&#xff0c;计算机的影响是全面且深入的。人们生活水平的不断提高&#xff0c;日常生活中人们对高校共享机房管理方面的要求也在不断提高&#xff0c;需要高校共享机房的人数更是不断增加&#xff0c;使得高校共享机房管理…

Swift Core Data 分阶段迁移

文章目录 前言什么是分阶段迁移&#xff1f;提供一些背景信息创建迁移管理器设置使用 Core Data 栈。总结 前言 在这之前&#xff0c;我发布了一篇文章&#xff0c;在其中解释了如何使用映射模型和自定义迁移策略执行复杂的 Core Data 迁移。虽然这种方法性能良好且运行良好&a…

阿里巴巴矢量图标库使用

阿里巴巴矢量图标库官网 添加图标到购物车 悬浮到图标上面会有个购物车icon,点击一下就可以添加购物车了 添加图标到项目 添加完购物车后,右上角会有当前在购物车的数量,点击右上角购物车icon,在新弹窗内点击添加至项目,选择添加到哪个项目(没有项目就创建一个),点击完成,…

C++ 教程 - 08 文件操作与异常处理

文章目录 文件操作文件对象其他方法异常处理 文件操作 需要头文件 <iostream><fstream> 读取文件 ifstream obj; obj.open(const char* filename, std::in)写入文件ofstream obj; obj.open(const char* filename, std::out)读、写文件 fstream&#xff0c;包含了i…

免杀笔记 ---> PE

本来是想先把Shellcode Loader给更新了的&#xff0c;但是涉及到一些PE相关的知识&#xff0c;所以就先把PE给更了&#xff0c;后面再把Shellcode Loader 给补上。 声明&#xff1a;本文章内容来自于B站小甲鱼 1.PE的结构 首先我们要讲一个PE文件&#xff0c;就得知道它的结构…

MySQL之备份与恢复(四)

备份与恢复 存储引擎和一致性 3.复制 从备库中备份最大的好处是可以不干扰主库&#xff0c;避免在主库上增加额外的负载。这是一个建立备库的好理由&#xff0c;即使不需要用它做负载均衡或高可用。如果钱是个问题&#xff0c;也可以把备份用的备库用于其他用户&#xff0c;…

​香橙派AIpro测评:usb鱼眼摄像头的Camera图像获取

一、前言 近期收到了一块受到业界人士关注的开发板"香橙派AIpro",因为这块板子具有极高的性价比&#xff0c;同时还可以兼容ubuntu、安卓等多种操作系统&#xff0c;今天博主便要在一块832g的香橙派AI香橙派AIpro进行YoloV5s算法的部署并使用一个外接的鱼眼USB摄像头…

小龙虾优化24种机器学习多输入单输出回归|时序预测模型

小龙虾优化24种机器学习多输入单输出回归|时序预测模型 文章目录 小龙虾优化24种机器学习多输入单输出回归|时序预测模型前言一、小龙虾优化基本原理二、优化机器学习模型1.COA-CNN-BiGRU-Attention回归模型2.基于小龙虾优化支持向量机的数据回归预测Matlab程序COA-SVM 多特征输…

Android EditText的属性与用法

EditText 是编辑框控件&#xff0c;可以接收用户输入&#xff0c;并在程序中对用户输入进行处理。EditText在App里随处可见&#xff0c;在进行搜索、聊天、拨号等需要输入信息的场合&#xff0c;都可以使用 EditText。 图1 编辑框示意图 EditText 是TextView的子类&#xff0c…

sql语句练习注意点

1、时间可以进行排序&#xff0c;也可以用聚合函数对时间求最大值max&#xff08;时间&#xff09; 例如下面的例子&#xff1a;取最晚入职的人&#xff0c;那就是将入职时间倒序排序&#xff0c;然后limit 1 表&#xff1a; 场景&#xff1a;查找最晚入职员工的所有信息 se…

【unity实战】使用Unity实现动作游戏的攻击 连击 轻重攻击和打击感

最终效果 文章目录 最终效果前言素材下载&#xff1a;玩家移动跳跃控制攻击动画配置轻攻击重攻击 攻击时禁止移动和攻击移动补偿敌人击退和播放受击动画受击特效攻击停顿和屏幕震动局部顿帧&#xff08;补充&#xff09;参考源码完结 前言 注意本文为自己的学习记录笔记&#…

日志自动提取---七牛Logkit观星应急工具

目录 七牛Logkit (Windows&Linux&Mac 等) 下载: 文档: windows配置过程: 1-下载 2-修改logkit-community基本配置 3-启动! 4-浏览器访问 5-添加配置吧 观星应急工具 &#xff08;Windows 系统日志&#xff09; 七牛Logkit (Windows&Linux&Mac 等) -…