spdlog日志库--源码解析

系列目录
spdlog日志库–基础介绍
spdlog日志库–源码解析
spdlog日志库–输出格式(fmt 库集成)
spdlog日志库–使用案例

本文主要目的是对spdlog v1.13.0的源码进行分析,以便后续更好使用spdlog日志库。参考:链接

logger

以调用logger.warn为例,其调用栈基本可分为如下两类:
无需字符串格式化时

logger.warn("hello world!");
调用栈:
template <typename T>
void warn(const T &msg)
    --> void log(level::level_enum lvl, string_view_t msg); // 增加日志等级作为参数传入
        --> void log(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结构体中
                --> void logger::log_it_(const spdlog::details::log_msg &log_msg, bool log_enabled, bool traceback_enabled);
                    --> void logger::sink_it_(const details::log_msg &msg); // 将封装后的details::log_msg结构体投递给下游的sink

需要字符串格式化时

logger.warn("hello world! {}", "pond-flower");
调用栈:
template <typename... Args>
void warn(format_string_t<Args...> fmt, Args &&...args);
    --> template <typename... Args>
        void log(level::level_enum lvl, format_string_t<Args...> fmt, Args &&...args); // 增加日志等级作为参数传入
        --> template <typename... Args>
            void log(source_loc loc, level::level_enum lvl, format_string_t<Args...> fmt, Args &&...args); // 增加文件名、函数名、文件行号作为参数传入
            --> template <typename... Args>
                void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&...args); // 字符串格式化后将日志信息封装至details::log_msg结构体中
                --> void logger::log_it_(const spdlog::details::log_msg &log_msg, bool log_enabled, bool traceback_enabled);
                    --> void logger::sink_it_(const details::log_msg &msg); // 将封装后的details::log_msg结构体投递给下游的sink

结合上述调用栈可看出两种日志记录形式殊途同归,最终将日志信息封装至details::log_msg后投递给下游的sink进行处理。
其中对日志信息最重要的处理行为是将日志信息封装至details::log_msg以及将封装后的detail::log_msg结构体投递给下游的sink。

其源码如下:

template <typename... Args>
    void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&...args) {
        bool log_enabled = should_log(lvl); // 判断是否需要记录日志(根据日志级别)
        bool traceback_enabled = tracer_.enabled(); // 判断是否需要traceback
        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(args...)); // 字符串格式化处理(使用std::fmt)
#else
            fmt::vformat_to(fmt::appender(buf), fmt, fmt::make_format_args(args...)); // 字符串格式化处理(使用第三方fmt库)
#endif

            details::log_msg log_msg(loc, name_, lvl, string_view_t(buf.data(), buf.size())); // 封装日志信息至log_msg对象中
            log_it_(log_msg, log_enabled, traceback_enabled); // 将log_msg对象投递给sink进行处理
        }
        SPDLOG_LOGGER_CATCH(loc)
    }
SPDLOG_INLINE void logger::sink_it_(const details::log_msg &msg) {
    // 遍历所有sink,将msg交由各个sink进行处理
    // 单个logger对象可对应多个sink——即单个输入端允许对应多个输出端
    // 以此实现了同份日志内容可输出至日志文件、控制台等
    for (auto &sink : sinks_) {
        if (sink->should_log(msg.level)) {
            SPDLOG_TRY { sink->log(msg); }
            SPDLOG_LOGGER_CATCH(msg.source)
        }
    }

    // 根据日志级别是否超出flush_level_判断是否需要立即对sink进行flush操作
    if (should_flush_(msg)) {
        flush_();
    }
}

总结
logger的实现中大量使用了可变参数模板与模板实例化技术,逐步将日志所需要的信息逐层传递,同时通过模板内联避免了函数逐层传递带来的性能损耗,代码简洁易懂、扩展性强,极具参考意义。

async-logger

async-logger继承自logger,通过重写父类的sink_it_flush_以实现由线程池执行sink->log(msg)sink->flush(),实现了异步日志打印。
因此async-logger相较于logger新增了两个成员变量。

std::weak_ptr<details::thread_pool> thread_pool_; // 线程池对象
async_overflow_policy overflow_policy_; // 日志队列满时处理策略:阻塞、丢弃新日志、丢弃旧日志

重写后的sink_it_源码如下

> SPDLOG_INLINE void spdlog::async_logger::sink_it_(const
> details::log_msg &msg){
>     SPDLOG_TRY{if (auto pool_ptr = thread_pool_.lock()){
>         pool_ptr->post_log(shared_from_this(), msg, overflow_policy_); // 将日志消息投递到线程池的消息队列中 } else {
>     throw_spdlog_ex("async log: thread pool doesn't exist anymore"); } } SPDLOG_LOGGER_CATCH(msg.source) }

重写后的flush_源码如下:

SPDLOG_INLINE void spdlog::async_logger::flush_()
{
    SPDLOG_TRY{if (auto pool_ptr = thread_pool_.lock())
    {
        pool_ptr->post_flush(shared_from_this(), overflow_policy_); // 将flush请求投递到线程池的消息队列中
	}
	else {
    	throw_spdlog_ex("async flush: thread pool doesn't exist anymore");
	}
	SPDLOG_LOGGER_CATCH(source_loc());
}

sink

sink模块相关代码位于sinks目录下,spdlog提供了多种sink实现,以此支持向控制台、文件、数据库、消息队列等目的端输出日志。
其类继承模式基本如下:

sink // 虚基类:用于为外部调用提供统一接口
    -> basic_sink // 模板类:用于封装线程安全的接口
        // 具体sink实现
        -> basic_file_sink
        -> hourly_file_sink
        -> daily_file_sink
        -> rotating_file_sink
    -> stdout_sink_base // 模板类:用于封装线程安全的接口(主要用于控制台的输出)
        -> stdout_sink
        -> stderr_sink

sink类

是所有不同类型sink的虚基类,用于提供统一的接口封装,其定义如下:

class SPDLOG_API sink {
public:
    virtual ~sink() = default;
    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; // 根据日志级别判断是否需要记录日志

protected:
    // sink log level - default is all
    level_t level_{level::trace}; // 原子变量以保证日志等级获取、判断的线程安全性
};

从上述定义上可看出,sink其实并没有在内部进行加锁等操作以在基类中保证log与flush调用的线程安全,实际上线程安全是在base_sink提供的。

base_sink类

base_sink继承自sink,是模板类,支持自定义锁类型,相较于sink而言主要在各处操作前通过自定义锁保证线程安全。
主要实现如下:

template <typename Mutex>
class SPDLOG_API base_sink : public sink {
public:
    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);
};
template <typename Mutex>
void SPDLOG_INLINE spdlog::sinks::base_sink<Mutex>::log(const details::log_msg &msg) {
    std::lock_guard<Mutex> lock(mutex_);
    sink_it_(msg);
}

template <typename Mutex>
void SPDLOG_INLINE spdlog::sinks::base_sink<Mutex>::flush() {
    std::lock_guard<Mutex> lock(mutex_);
    flush_();
}

由于base_sink为模板类实现,可自定义锁类型,如使用互斥锁、读写锁等,并且可以优雅的使base_sink同时支持无锁版本与有锁版本,以basic_file_sink为例,其具体实现方式如下:

struct null_mutex {
    void lock() const {}
    void unlock() const {}
};

using basic_file_sink_mt = basic_file_sink<std::mutex>; // 有锁版本,适用于多线程场景
using basic_file_sink_st = basic_file_sink<details::null_mutex>; // 无锁版本,适用于单线程场景

basic_file_sink类

basic_file_sink继承自basic_sink,是将日志输出至单个目标文件的具体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); // 将log_msg格式化为字符串
    file_helper_.write(formatted);
}

template <typename Mutex>
SPDLOG_INLINE void basic_file_sink<Mutex>::flush_() {
    file_helper_.flush();
}

以文件作为输出端的sink类还有hourly_file_sink、daily_file_sink、rotating_file_sink等,实现上基本与basic_file_sink类似,仅是在其之上提供了文件转储等能力。

stdout_sink_base类

stdout_sink_base类似于basic_sink,其与basic_sink的主要区别在于其成员变量mutex_类型不同,其中stdout_sink_base的成员变量mutex_为引用类型,而basic_sink的成员变量mutex_为非引用类型。
由此可看出stdout_sink_base主要应用于多个sink对象共享同个互斥量的场景,而控制台的输出则是上述场景的典型示例。
stdout_sink_base同样重写了父类sink的log与flush接口。
具体实现如下:

template <typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::log(const details::log_msg &msg) {
#ifdef _WIN32
    if (handle_ == INVALID_HANDLE_VALUE) {
        return;
    }
    std::lock_guard<mutex_t> lock(mutex_);
    memory_buf_t formatted;
    formatter_->format(msg, formatted);
    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
    std::lock_guard<mutex_t> lock(mutex_);
    memory_buf_t formatted;
    formatter_->format(msg, formatted);
    ::fwrite(formatted.data(), sizeof(char), formatted.size(), file_);
#endif                // WIN32
    ::fflush(file_);  // flush every line to terminal
}

template <typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::flush() {
    std::lock_guard<mutex_t> lock(mutex_);
    fflush(file_);
}

formatter

在介绍sink时其实已经有使用formatter的示例:

memory_buf_t formatted;
formatter_->format(msg, formatted);

formatter将log_msg结构体以预定义的格式转化为字符串存放于formatted中。

formatter类

类似于sink模块的sink类,同样为虚基类,用于为外部提供统一的接口,其定义如下:

class formatter {
public:
    virtual ~formatter() = default;
    virtual void format(const details::log_msg &msg, memory_buf_t &dest) = 0;
    virtual std::unique_ptr<formatter> clone() const = 0;
};

上述formatter_实际类型为pattern_formatter对象的智能指针,但在介绍pattern_formatter前,先来了解下flag_formatter。

flag_formatter类

先来看如何设置日志格式:

spdlog::set_pattern("[%H点%M分]");
调用栈:
void logger::set_pattern(std::string pattern, pattern_time_type time_type = pattern_time_type::local); // 构造pattern_formatter对象的智能指针
    --> void logger::set_formatter(std::unique_ptr<formatter> f); // 遍历logger中所有的sink,依次调用sink->set_formatter
        --> void sink::set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter); //

关键步骤为pattern_formatter对象的初始化,其实现如下:

// 自定义格式
// 自定义格式
SPDLOG_INLINE pattern_formatter::pattern_formatter(std::string pattern,
                                                   pattern_time_type time_type,
                                                   std::string eol,
                                                   custom_flags custom_user_flags)
    : pattern_(std::move(pattern)),
      eol_(std::move(eol)),
      pattern_time_type_(time_type),
      need_localtime_(false),
      last_log_secs_(0),
      custom_handlers_(std::move(custom_user_flags)) {
    std::memset(&cached_tm_, 0, sizeof(cached_tm_));
    compile_pattern_(pattern_); // 解析pattern_
}

// 默认格式
SPDLOG_INLINE pattern_formatter::pattern_formatter(pattern_time_type time_type, std::string eol)
    : pattern_("%+"),
      eol_(std::move(eol)),
      pattern_time_type_(time_type),
      need_localtime_(true),
      last_log_secs_(0) {
    std::memset(&cached_tm_, 0, sizeof(cached_tm_));
    formatters_.push_back(details::make_unique<details::full_formatter>(details::padding_info{}));
}

解析pattern_是通过compile_pattern_实现的,具体实现如下:

SPDLOG_INLINE void pattern_formatter::compile_pattern_(const std::string &pattern) {
    auto end = pattern.end();
    std::unique_ptr<details::aggregate_formatter> user_chars;
    formatters_.clear();
    // 遍历pattern
    for (auto it = pattern.begin(); it != end; ++it) {
        if (*it == '%') {
            if (user_chars)
            {
                formatters_.push_back(std::move(user_chars)); // 将flag_formatter追加入formatters_
            }

            // 字符串对齐处理相关
            auto padding = handle_padspec_(++it, end);

            if (it != end) {
                if (padding.enabled()) {
                    handle_flag_<details::scoped_padder>(*it, padding);
                } else {
                    handle_flag_<details::null_scoped_padder>(*it, padding);
                }
            } else {
                break;
            }
        } else  // chars not following the % sign should be displayed as is
        {
            if (!user_chars) {
                user_chars = details::make_unique<details::aggregate_formatter>();
            }
            user_chars->add_ch(*it); // 将当前字符追加入user_chars之后
        }
    }
    if (user_chars)  // append raw chars found so far
    {
        formatters_.push_back(std::move(user_chars));
    }
}

pattern_formatter

pattern_formatter利用预处理过的pattern来完成字符串格式化步骤,具体实现如下:

SPDLOG_INLINE void pattern_formatter::format(const details::log_msg &msg, memory_buf_t &dest) {
    if (need_localtime_) { // 是否需要记录当前时间
        const auto secs =
            std::chrono::duration_cast<std::chrono::seconds>(msg.time.time_since_epoch());
        if (secs != last_log_secs_) {
            cached_tm_ = get_time_(msg); // 更新缓存时间
            last_log_secs_ = secs;
        }
    }

    for (auto &f : formatters_) { // 遍历formatters_(std::vector<std::unique_ptr<details::flag_formatter>>)
        f->format(msg, cached_tm_, dest); // 不断向dest中追加信息
    }
    // write eol
    details::fmt_helper::append_string_view(eol_, dest);
}

参考链接

链接

spdlog是一个快速、异步的C++日志,支持多线程和跨平台,具有简单易用的接口和高性能的日志记录能力。以下是spdlog的一些主要特点: 1. 快速:spdlog使用高效的缓冲区实现快速的日志记录,可以轻松处理高负载的日志记录场景。 2. 异步:spdlog支持异步日志记录,可以将日志写入缓冲区后立即返回,不会阻塞主线程。 3. 多线程:spdlog可以安全地在多个线程中使用,支持多个线程同时进行日志记录,且不需要额外的同步机制。 4. 跨平台:spdlog可以在多个平台上运行,包括Windows、Linux、OS X等。 5. 简单易用:spdlog提供简洁明了的API,可以轻松地实现日志记录功能。 以下是spdlog的使用示例: ```cpp #include "spdlog/spdlog.h" void log_example() { // 创建一个控制台日志记录器 auto console = spdlog::stdout_color_mt("console"); // 创建一个文件日志记录器 auto file = spdlog::basic_logger_mt("file_logger", "logs/mylogfile.txt"); // 设置日志记录级别 console->set_level(spdlog::level::info); file->set_level(spdlog::level::trace); // 记录日志 console->info("Hello, spdlog!"); file->trace("This is a trace message."); } ``` 以上代码演示了如何创建一个控制台日志记录器和一个文件日志记录器,并设置不同的日志记录级别,最后分别记录了一条信息和一条跟踪信息。 更多关于spdlog的使用说明,请参考spdlog的官方文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值