spdlog源码剖析:高性能日志库设计原理 | C++开源项目解析

spdlog源码分析:高性能日志库设计原理 #

引言 #

spdlog是一个广受欢迎的C++日志库,以其高性能和灵活性著称。本文深入分析其源码实现。

架构 #

spdlog架构图

如图,其实也不只是spdlog,几乎所有的Log模块都是这样的架构:

  • Macro:Log的宏,主要用于封装Logger的函数调用,通过宏可以记录下来当前的文件名和行号等信息,然后可以当做参数传递到函数中。
  • Logger:Logger对象作为一个组合体,组合Format和Sink,内部会先调用Format,将Format后的Msg调用Sink,写到某个地方或某些地方。
  • Resolve:Resolve是个函数,它的主要功能就是把用户输入的信息拼接成字符串:
    • 用户可能有流式输出,比如 std::cout << 1 << "a" << 1;,那拼接后的字符串就是1a1
    • 用户可能有python式输出,比如 printf("{}你好{}", 1, 2);那拼接后的字符串就是1你好2。
  • Format:用于格式化日志信息,只打印用户输入的信息还不够,我们还需要附加行号、文件名、进程ID、线程ID、时间戳、日志Level等信息,这样才方便根据日志分析问题。
    • 比如用户打印的日志是’你好SPDLOG’,那日志中可能输出的是
      • 2024-03-28 10:01:01 PID(123) TID(234) [INFO] hello_world.cc:123 你好SPDLOG
    • 当然,我们可以自定义Format格式,实现自己的需求。
  • Sink:用于控制日志写入的位置,比如写到控制台、写到文件、写到Logcat等等地方,我们也可以自定义写入的位置,比如Sink到某个网络服务器上,也可以一份日志输出到多个位置。

了解这个架构后,我们直接去源码中找各个模块对应的位置就可以。

Log的宏在哪里? #

#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_INFO
#define SPDLOG_LOGGER_INFO(logger, ...) \
    SPDLOG_LOGGER_CALL(logger, spdlog::level::info, __VA_ARGS__)
#define SPDLOG_INFO(...) SPDLOG_LOGGER_INFO(spdlog::default_logger_raw(), __VA_ARGS__)
#else
#define SPDLOG_LOGGER_INFO(logger, ...) (void)0
#define SPDLOG_INFO(...) (void)0
#endif

这里可以看到 SPDLOG_INFO的宏,会转到SPDLOG_LOGGER_CALL宏上,并且携带了spdlog::default_logger_raw() 和 日志的info级别,再看SPDLOG_LOGGER_CALL的定义:

#ifndef SPDLOG_NO_SOURCE_LOC
#define SPDLOG_LOGGER_CALL(logger, level, ...) \
    (logger)->log(spdlog::source_loc{__FILE__, __LINE__, SPDLOG_FUNCTION}, level, __VA_ARGS__)
#else
#define SPDLOG_LOGGER_CALL(logger, level, ...) \
    (logger)->log(spdlog::source_loc{}, level, __VA_ARGS__)
#endif

通过SPDLOG_LOGGER_CALL的定义,可以知道SPDLOG_INFO,最终会调用 spdlog::default_logger_raw()->log(spdlog::source_loc{__FILE__, __LINE__, SPDLOG_FUNCTION}, spdlog::level::info, __VA_ARGS__)那spdlog::default_logger_raw()是什么?了解上面的架构后,我们肯定知道,这个就是架构中的Logger对象。此处代码详见:spdlog.h#L323

Logger对象源码在哪里? #

为方便理解,我删减了大部分代码,保留关键部分:

class SPDLOG_API logger {
public:
    // Logger with sinks init list
    logger(std::string name, sinks_init_list sinks)
        : logger(std::move(name), sinks.begin(), sinks.end()) {}
    void set_formatter(std::unique_ptr<formatter> f);
protected:
    std::string name_;
    std::vector<sink_ptr> sinks_;
    spdlog::level_t level_{level::info};
    // 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);
        bool traceback_enabled = tracer_.enabled();
        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...));
#else
            fmt::vformat_to(fmt::appender(buf), fmt, fmt::make_format_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)
    }
};

可以看到:

  1. 构造函数中可以配置Sink的位置,因为是list,所以可以配置多个Sink
  2. 通过set_formatter,可以任意配置Formatter

我们主要看log_方法,它接收几个参数:

  1. source_loc:主要包含文件名、函数名、行号等信息,在Log Macro宏处会自动构造这个对象
  2. level_enum:日志级别,日志级别低于配置的级别时,不会输出
  3. fmt & args:实际的日志内容。

log_中首先根据日志级别判断是否要写日志,详见:

bool should_log(level::level_enum msg_level) const {
    return msg_level >= level_.load(std::memory_order_relaxed);
}

而 fmt::vformat_to对应架构图中的resolve,这个fmt也进入了C++20标准,感兴趣的可以了解下。还剩 Formatter 和 Sink 的流程没有介绍,继续看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);
    }
}

那Formatter去哪里了?其实在sink里,拿个 basic-file-sink 举例:

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);
}

详见:basic_file_sink-inl.h

Formatter代码在哪里? #

看代码:

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;
};

spdlog提供了标准的formatter,我们也可以自定义formatter,然后配置给logger。详见:formatter.h

Sink代码在哪里? #

namespace sinks {
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};
};

从图中看,spdlog提供了好多个sink,我们可以选择性使用。至此,整个spdlog的架构已经清晰,在源码中也已经定位到具体模块,细节就不多介绍,细节大家自己看就可以。