spdlog 源码解析

spdlog是开源、高性能、跨平台,支持header-only/compiled的C++日志库。

本文主要目的是对spdlog v1.13.0的源码进行分析(编译运行环境为6.5.0-18-generic #18~22.04.1-Ubuntu),以探讨spdlog如何构建高性能、可扩展的日志框架的。

github链接

  gabime/spdlog: Fast C++ logging library. (github.com)

应用示例

  spdlog/README.md at v1.x · gabime/spdlog (github.com)

spdlog代码特点

  spdlog主要基于C++11开发(若编译环境支持C++20,则将使用std::fmt取代第三方fmt库)

  spdlog中大量使用移动语义、完美转发以减少对象拷贝,又利用内联、模板等技术尽量减少了抽象的代价

  同时广泛使用了智能指针等降低了内存管理的复杂性,通过spdlog可以深入的了解C++11的优雅实现

spdlog架构

  • logger/async_logger

    日志处理的入口,负责格式化日志信息、日志信息的整理合并(如日志级别、文件名、函数名、文件行号等),最终封装至log_msg对象中,再将log_msg对象投递给下游处理。

    logger与aync_logger区别在于:

      logger是同步处理,会由调用日志记录的线程直接将封装后的log_msg对象投递给下游的sink

      aync_logger则是异步处理,调用日志记录的线程仅负责将封装后的log_msg对象放入线程安全队列,后续由线程池从线程安全队列中不断处理队列中的日志对象

 

  • sink

 

    负责接收log_msg对象,并通过formatter将对象中记录的信息转换为字符串,最终将字符串输出到目标位置(控制台、日志文件等)

  • formatter

    负责将log_msg对象中的信息转换成字符串

  • registry

    负责管理所有的logger(创建、销毁、获取等),并且通过registry还可对所有的logger做全局设置

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);
}
复制代码

 

posted @   静塘花开  阅读(900)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示