前言

本系列旨在通过解析开源项目,学习优雅、简洁的实现,讨论其使用场景,覆盖的范围一般不会超过项目的实现,配合代码阅读更佳

项目简介

spdlog是一个C++日志库,支持header-only以及库引入两种方式,在工程实践上广受好评。

项目链接: GitHub Repository gabime/spdlog 在 GitHub 上查看该项目。

基本使用

对于较为复杂的库,在学习实现之前,最好从使用开始,spdlog也是如此。

引入

为了简单考虑,在使用时使用header-only方式引入,CMakeLists.txt参考如下:

 1cmake_minimum_required(VERSION 3.15)
 2project(my_app LANGUAGES CXX)
 3
 4add_library(spdlog_header_only INTERFACE)
 5target_include_directories(spdlog_header_only INTERFACE
 6    "${CMAKE_CURRENT_SOURCE_DIR}/third_party/spdlog/include"
 7)
 8set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 9add_executable(my_app main.cpp)
10target_compile_features(my_app PRIVATE cxx_std_11)
11# 只影响宏
12target_compile_definitions(
13    my_app
14    PRIVATE
15    SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_TRACE
16)
17target_link_libraries(my_app PRIVATE spdlog_header_only)

引入方式也很有趣,spdlog提供了两种不同的引入方式,header-only的方式默认所有实现都已在头文件中。

这时候就应该问第一个问题了:既然都在头文件中,常规来讲,头文件是不编译成库的(当然也有办法),那spdlog是如何实现的呢? 这里就不得不提到spdlog精巧的设计了,观察其文件命名,很快就可以发现有两种.h文件,一种是常规.h文件,另外一种是以-inc.h结尾的头文件。 以spdlog.h和spdlog-inc.h为例,我们可以在spdlog.h以及common.h处找到如下代码:

 1// spdlog.h
 2#ifdef SPDLOG_HEADER_ONLY
 3#include "spdlog-inl.h"
 4#endif
 5
 6// common.h
 7#ifdef SPDLOG_COMPILED_LIB
 8...
 9#else  // !defined(SPDLOG_COMPILED_LIB)
10#define SPDLOG_API
11#define SPDLOG_HEADER_ONLY
12#define SPDLOG_INLINE inline
13#endif  // #ifdef SPDLOG_COMPILED_LIB

可以看到,两种引入方式是通过宏SPDLOG_HEADER_ONLY控制的,而宏又间接影响了另外几个宏的定义,进而影响spdlog-inc.h的引入,而该文件正是存放了一些函数的定义,承担了.cpp文件的功能。

结合这一点,再看看spdlog.cpp,只需要引入必要的头文件,在预处理后,便引入了函数、方法的定义,便可以作为一个编译单元。 spdlog通过控制SPDLOG_COMPILED_LIBSPDLOG_HEADER_ONLY两个宏,做到了支持两种导入方式。

顺带吐槽一下,C++这种上古时代的引入方式真是应该被扫进历史的垃圾堆。

使用

spdlog的使用也相当简单,以如下代码为例:

1spdlog::info("welcome to spdlog");
2// fmt的格式化,空的就是表示默认格式
3spdlog::error("Some error message with arg: {}", 1);
4// :表达格式控制,d是decimal,最小字宽为8,不足的用0填充
5spdlog::warn("Easy padding in numbers like {:08d}", 12);
6// 0表示参数编号,d是decimal,x是hex,b是oct
7spdlog::critical("Support for int: {0:d};  hex: {0:x};  oct: {0:o}; bin: {0:b}", 42);

对应到输出则是:

1[2026-08-16 19:00:04.070] [info] welcome to spdlog
2[2026-08-16 19:00:04.071] [error] Some error message with arg: 1
3[2026-08-16 19:00:04.071] [warning] Easy padding in numbers like 00000012
4[2026-08-16 19:00:04.071] [critical] Support for int: 42;  hex: 2a;  oct: 52; bin: 101010

对于前面的日期和时间是系统自带的,成为metadata,后面的输出信息则是用户的输出,也被称为payload。

架构

一层层剥离,我们便可以看到spdlog的构成: 以上方的spdlog::info和spdlog::error为例,实现分别如下:

 1// spdlog.h
 2template <typename T>
 3inline void info(const T &msg) {
 4    default_logger_raw()->info(msg);
 5}
 6
 7template <typename... Args>
 8inline void error(format_string_t<Args...> fmt, Args &&...args) {
 9    default_logger_raw()->error(fmt, std::forward<Args>(args)...);
10}
11
12// spdlog-inc.h
13SPDLOG_INLINE logger *default_logger_raw() {
14    // 为了更快一点,取的是原生指针
15    return details::registry::instance().get_default_raw();
16}
17
18// registry-inc.h
19SPDLOG_INLINE logger *registry::get_default_raw() { return default_logger_.get(); }

显然,对于基础格式,直接调用一个参数的模板函数即可,而对于多个参数,则需要可变参数的支持。 在上述两个实现中,我们不难看到,都是调用了全局的默认logger。 在spdlog中,对象的管理层级大致如下:

  1. registry: 单例,通过logger的名称为key管理所有logger对象
  2. logger: 负责处理fmt相关的工作,处理格式化相关,创建一条消息对象
  3. sink: 负责将一条消息对象转发至配置的文件(stdout、stderr、普通file等等),一个logger也许会有多个sink,以将一条消息输出到不同的目标,当然也是在该层做到最终的输出
  4. formatter: 帮助sink管理输出前缀格式,也就是日志的metadata
  5. flag_formatter: 针对处理自定义的metadata格式,也就是pattern

从上面的分级可以看到spdlog的设计简洁之处,每个对象类型都有自己的职责(单一职责原则),通过一个消息对象传递消息,层层递进。 在这种架构下,如果代码新增了一个模块,我可以新增logger,来处理不同的日志源;而针对一个logger,我既希望它输出到终端,又希望它输出到文件,就增加sink。当然,如果metadata有变化,应当也增加对应的sink来处理。

此处应当有一个图。

flowchart LR R@{ shape: rect, label: "registry" } L@{ shape: processes, label: "logger" } S@{ shape: processes, label: "sink" } F@{ shape: rect, label: "formatter" } PF@{ shape: rect, label: "pattern_formatter" } FF@{ shape: processes, label: "flag_formatter" } R -->|管理| L L -->|持有| S S -->|持有| F F -->|具体实现| PF PF -->|持有| FF R -.-> F classDef registryStyle fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E,stroke-width:2px; classDef loggerStyle fill:#EDE9FE,stroke:#7C3AED,color:#4C1D95,stroke-width:2px; classDef sinkStyle fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:2px; classDef formatterStyle fill:#FFEDD5,stroke:#EA580C,color:#7C2D12,stroke-width:2px; classDef flagStyle fill:#FEF3C7,stroke:#D97706,color:#78350F,stroke-width:2px; class R registryStyle; class L loggerStyle; class S sinkStyle; class F,PF formatterStyle; class FF flagStyle; linkStyle default stroke:#94A3B8,stroke-width:1.5px;
后续将从一条基本的日志出发,追踪其在各个层级的生命周期。

registry

spdlog的基础管理类,也是经典单例实现,除了管理所有logger,还负责管理全局输出等级,每个logger的输出等级等等。 在registry被构造时,会顺手构造一个默认的logger,如果用不上更多功能的话,这个default_logger就可以完成几乎所有的功能。

 1SPDLOG_INLINE registry::registry()
 2    : formatter_(new pattern_formatter()) {
 3    // 创建一个sink
 4    // 以此为切入点看sink
 5    auto color_sink = std::make_shared<sinks::ansicolor_stdout_sink_mt>();
 6
 7    const char *default_logger_name = "";
 8    // 创建一个default logger,空名称,sink被logger控制
 9    default_logger_ = std::make_shared<logger>(default_logger_name, std::move(color_sink));
10    loggers_[default_logger_name] = default_logger_;
11
12#endif  // SPDLOG_DISABLE_DEFAULT_LOGGER
13}

删掉了无用的部分后,让我们只关注logger的创建,这里并没有太多的子类,因为logger的任务很简单,只关注格式化相关的任务。另外,这个logger的名称是匿名的,也只有一个简单的ansicolor_stdout_sink_mt,这里的mt表示是multi-thread,意味着其支持多线程。同样,这里也要提出一个新问题,为什么没有logger_mt呢?因为logger的任务用不上多线程,也没有什么线程不安全的点。

从上文可以看到,对于常规的spdlog::xxx之类的场景,默认logger就可以完成了。

logger

上文反复说过,logger负责的是格式化的部分,spdlog引入的是fmt库的格式化,格式化的细节语法并不在本文讨论范围中,以后有机会可以再分析下fmt库。

在上文中,我们说到对于是否带格式化的参数,有两套处理逻辑,分别是:

 1// logger.h
 2// 不带格式化信息就走这些
 3template <typename T>
 4void trace(const T &msg) {
 5    log(level::trace, msg);
 6}
 7
 8// 带格式化的走下面的
 9template <typename... Args>
10void trace(format_string_t<Args...> fmt, Args &&...args) {
11    log(level::trace, fmt, std::forward<Args>(args)...);
12}

一级转发

根据不同参数,trace实例化的也是两套模版来重载,那么对于log,也是如此,也会分出两套出来:

 1// logger.h
 2// 对应不带格式化的模板,这里log也是第一层
 3// 非格式化一级转发入口,非string类型
 4template <typename T>
 5void log(level::level_enum lvl, const T &msg) {
 6    log(source_loc{}, lvl, msg);
 7}
 8
 9// 对应带格式化的模板,即第一层log
10// 一级转发入口
11template <typename... Args>
12void log(level::level_enum lvl, format_string_t<Args...> fmt, Args &&...args) {
13    log(source_loc{}, lvl, fmt, std::forward<Args>(args)...);
14}
15
16// common.h
17struct source_loc {
18    SPDLOG_CONSTEXPR source_loc() = default;
19    SPDLOG_CONSTEXPR source_loc(const char *filename_in, int line_in, const char *funcname_in)
20        : filename{filename_in},
21          line{line_in},
22          funcname{funcname_in} {}
23
24    SPDLOG_CONSTEXPR bool empty() const SPDLOG_NOEXCEPT { return line <= 0; }
25    const char *filename{nullptr};
26    int line{0};
27    const char *funcname{nullptr};
28};

这里我们姑且将这个log称之为一级转发,可以看到,参数也是完美对应,这里有个小知识点:forward的使用,但这里就不展开了,值得注意一下。 我们可以观察到,形式上两者还是较为统一的,可以基本认为msg = fmt + args,后续也是这样处理的。 另外一个值得注意的点是,source_loc这样一个结构的首次出现,作用是记录一些location信息文件、函名、行号。

二级转发(仅非格式化)

这里对于非格式化的二级转发还有两种情况,一种是可以转成string like类型的,一种是不能转的。 对于可以转的,直接构造消息对象log_msg,到log_it_提前输出即可。 对于不能转的,才需要处理其输出方式,这里是直接套一个{},相当于使用底层的fmt库来做

 1// logger.h
 2// 非格式化的二级转发入口,可以转换string like类型的
 3void log(source_loc loc, level::level_enum lvl, string_view_t msg) {
 4    bool log_enabled = should_log(lvl);
 5    bool traceback_enabled = tracer_.enabled();
 6    if (!log_enabled && !traceback_enabled) {
 7        return;
 8    }
 9
10    // 如果不需要格式化,直接输出即可
11    details::log_msg log_msg(loc, name_, lvl, msg);
12    log_it_(log_msg, log_enabled, traceback_enabled);
13}
14
15// 非格式化的二级转发入口,非string类型,比如int、bool之类的
16// T cannot be statically converted to format string (including string_view/wstring_view)
17template <class T,
18            typename std::enable_if<!is_convertible_to_any_format_string<const T &>::value,
19                                    int>::type = 0>
20void log(source_loc loc, level::level_enum lvl, const T &msg) {
21    // 不能转成字符串的,非格式化的转发入口
22    // 等价于给普通类型自动套上一个{},但一定要该类型是可以被格式化的
23    // 二级转发入口
24    log(loc, lvl, "{}", msg);
25}

本质上只是方便了一下,不用每次写logger->log(spdlog::level::info, "{}", 42);这种东西。

三级转发

针对非格式化(非string like类型的)和格式化,都是统一交由该层处理:

1// logger.h
2// 三级总入口,非格式化、格式化都会统一至此(做了些许简化)
3template <typename... Args>
4void log(source_loc loc, level::level_enum lvl, format_string_t<Args...> fmt, Args &&...args) {
5    log_(loc, lvl, fmt.get(), std::forward<Args>(args)...);
6}

三级的log作为统一入口,调用log_来实现格式化,删除冗余代码的结果如下:

 1// 专注解决格式化问题
 2// common implementation for after templated public api has been resolved
 3template <typename... Args>
 4void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&...args) {
 5    // 一条日志逻辑的起点
 6    bool log_enabled = should_log(lvl);
 7    bool traceback_enabled = tracer_.enabled();
 8    // 如果一条日志既没有被记录下来的价值,那当然直接返回
 9    if (!log_enabled && !traceback_enabled) {
10        return;
11    }
12    // 最终存放数据的buffer
13    memory_buf_t buf;
14    // make_format_args是打包参数,类型擦除,返回一个参数包对象,通常保存引用
15    // vformat_to是输出到指定缓冲区,在这里就是格式化到buf,这里也是格式化开始的阶段
16    fmt::vformat_to(fmt::appender(buf), fmt, fmt::make_format_args(args...));
17    // 现在,对于某条日志,有loc,有logger名称,有输出等级,有具体信息,最终构成了一条log_msg
18    // 但只有消息正文
19    details::log_msg log_msg(loc, name_, lvl, string_view_t(buf.data(), buf.size()));
20    // 最终通过这个函数输出
21    log_it_(log_msg, log_enabled, traceback_enabled);
22}

fmt::vformat_to这一步,所有的位置参数都被放到了msg中,也就是说构成了一条payload,将payload放入log_msg,则正式开始了层级的传递

log_it_

在上文中,非格式化的,可以转成string like的可以直接调用log_it_,而三级转发最终也是到了log_it_,现在可以看看log_it_的实现了:

 1void logger::log_it_(const details::log_msg &log_msg,
 2                                   bool log_enabled,
 3                                   bool traceback_enabled) {
 4    if (log_enabled) {
 5        sink_it_(log_msg);
 6    }
 7    // tracer底层是一个ring buffer
 8    if (traceback_enabled) {
 9        tracer_.push_back(log_msg);
10    }
11}
12
13void logger::sink_it_(const details::log_msg &msg) {
14    // 调用每个sink,来做具体的事项
15    for (auto &sink : sinks_) {
16        // sink也有自己的日志等级判断
17        if (sink->should_log(msg.level)) {
18            sink->log(msg); 
19        }
20    }
21
22    if (should_flush_(msg)) {
23        flush_();
24    }
25}

log_it_调用了sink_it_,来批量调用sink->log以实现日志输出,此时可以观察log_msg这个结构,来看看其作为msg的承载,究竟记录了哪些信息:

 1// log_msg.h
 2// 删除了方法
 3struct SPDLOG_API log_msg {
 4    string_view_t logger_name;  // 带logger name
 5    level::level_enum level{level::off};
 6    log_clock::time_point time;
 7    size_t thread_id{0};
 8
 9    // wrapping the formatted text with color (updated by pattern_formatter).
10    mutable size_t color_range_start{0};
11    mutable size_t color_range_end{0};
12    // log_msg带着全量的信息到处传递
13    source_loc source;
14    // payload是后一半的信息
15    string_view_t payload;
16};

这些信息都是构造时实时传入的,可以看到,最终还是构成了这么一份payload,这里的string_view_t可以当作标准库中的string_view来理解,本质上还是取的log_中的memory_buf_t中的数据指针(有数据扩容机制,小对象直接放栈上,大对象会放堆上)

至此,也算看完了logger的基础工作,就是把格式参数塞到格式化字符串里,来产生payload,当然省略了相当多的细节。

flowchart TD A["显式格式化日志
info('value={}', 42)"] P["单消息日志
log(level, 'hello')
或 log(level, 42)"] B["格式化入口
log(...) → log_()"] Q["添加默认 source_loc
log(source_loc{}, level, msg)"] R{"msg 是字符串类型?"} S["直接构造 log_msg"] N["自动套上 '{}'
转入格式化入口"] F["vformat_to 格式化正文"] G["构造 log_msg"] I["log_it_(log_msg)"] K["sink_it_(log_msg)
离开 logger 层"] A --> B P --> Q --> R R -->|是| S R -->|否| N --> B B --> F --> G S --> I G --> I I --> K classDef formattedStyle fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E,stroke-width:2px; classDef entryStyle fill:#F1F5F9,stroke:#64748B,color:#334155,stroke-width:2px; classDef stringStyle fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:2px; classDef objectStyle fill:#FEF3C7,stroke:#D97706,color:#78350F,stroke-width:2px; classDef commonStyle fill:#EDE9FE,stroke:#7C3AED,color:#4C1D95,stroke-width:2px; classDef exitStyle fill:#FFEDD5,stroke:#EA580C,color:#7C2D12,stroke-width:2px; class A,B,F,G formattedStyle; class P,Q,R entryStyle; class S stringStyle; class N objectStyle; class I commonStyle; class K exitStyle; linkStyle default stroke:#94A3B8,stroke-width:1.5px;

sink

同样,以默认logger的sink为例:

 1// registry-inl.h
 2SPDLOG_INLINE registry::registry()
 3    : formatter_(new pattern_formatter()) {
 4
 5    // 创建一个sink
 6    // 以此为切入点看sink
 7    auto color_sink = std::make_shared<sinks::ansicolor_stdout_sink_mt>();
 8
 9    const char *default_logger_name = "";
10    // 创建一个default logger,空名称,sink被logger控制
11    default_logger_ = std::make_shared<logger>(default_logger_name, std::move(color_sink));
12    loggers_[default_logger_name] = default_logger_;
13}
14
15// ansicolor_sink.h
16using ansicolor_stdout_sink_mt = ansicolor_stdout_sink<details::console_mutex>;
17
18template <typename ConsoleMutex>
19class ansicolor_stdout_sink : public ansicolor_sink<ConsoleMutex> {
20public:
21    explicit ansicolor_stdout_sink(color_mode mode = color_mode::automatic);
22};
23
24template <typename ConsoleMutex>
25class ansicolor_sink : public sink {
26    ...
27protected:
28    // 输出目标
29    FILE *target_file_;
30
31private:
32    mutex_t &mutex_;
33    bool should_do_colors_;
34    // 执行输出的对象
35    std::unique_ptr<formatter> formatter_;
36    std::array<std::string, level::n_levels> colors_;
37    void set_color_mode_(color_mode mode);
38    void print_ccode_(const string_view_t &color_code) const;
39    void print_range_(const memory_buf_t &formatted, size_t start, size_t end) const;
40    static std::string to_string_(const string_view_t &sv);
41}

省略方法之后,不难看到,这一层记录了输出目标,其他值得注意的只有以下几个:

  1. formatter_,负责格式化metadata
  2. print_ccode_,写入颜色控制指令
  3. print_range_,写入数据

那么,回到logger的最后一步,我们当然关注sink的log方法做了什么:

 1template <typename ConsoleMutex>
 2SPDLOG_INLINE void ansicolor_sink<ConsoleMutex>::log(const details::log_msg &msg) {
 3    // Wrap the originally formatted message in color codes.
 4    // If color is not supported in the terminal, log as is instead.
 5    std::lock_guard<mutex_t> lock(mutex_);
 6    msg.color_range_start = 0;
 7    msg.color_range_end = 0;
 8    memory_buf_t formatted;
 9    // 决定前一段元信息长什么样子
10    formatter_->format(msg, formatted);
11
12    // 将格式化的内存存入
13    if (should_do_colors_ && msg.color_range_end > msg.color_range_start) {
14        // 一个pattern里只支持一块着色区域,一般而言都是level 
15        // before color range
16        print_range_(formatted, 0, msg.color_range_start);
17        // in color range
18        print_ccode_(colors_.at(static_cast<size_t>(msg.level)));
19        print_range_(formatted, msg.color_range_start, msg.color_range_end);
20        print_ccode_(reset);
21        // after color range
22        print_range_(formatted, msg.color_range_end, formatted.size());
23    } else  // no color
24    {
25        print_range_(formatted, 0, formatted.size());
26    }
27    fflush(target_file_);
28}

根据上述的分析,print_range_、print_ccode_之类的函数只是在做输出,那显然真正的格式化是在formatter_->format(msg, formatted);中。 对于常规的ansicolor_sink,其formatter往往是pattern_formatter,于是我们可以继续往下探讨。

formatter

自然,这里会以pattern_formatter为例,也是整个库的精华所在,pattern_formatter的定义如下:

1// pattern_formatter.h
2class SPDLOG_API pattern_formatter final : public formatter {
3    // 包含预设+自定义的formatter
4    std::vector<std::unique_ptr<details::flag_formatter>> formatters_;
5    void format(const details::log_msg &msg, memory_buf_t &dest) override;
6
7    void compile_pattern_(const std::string &pattern);
8};

最重要的就只有compile_pattern_,这一步会在构造/设置的时候发生,对传入的pattern字符串做处理,以默认情况而言,传入的往往是"%+",但并不以此为例,为了直观对比其功能,假设传入的pattern是:

 1// [hour minute second zone] [logger name] [level] [thread num] message
 2spdlog::set_pattern("[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v");
 3
 4// pattern_formatter-inl.h
 5SPDLOG_INLINE void pattern_formatter::compile_pattern_(const std::string &pattern) {
 6    auto end = pattern.end();
 7    std::unique_ptr<details::aggregate_formatter> user_chars;
 8    formatters_.clear();
 9    for (auto it = pattern.begin(); it != end; ++it) {
10        if (*it == '%') {
11            if (user_chars)  // append user chars found so far
12            {
13                formatters_.push_back(std::move(user_chars));
14            }
15            // 解决宽度对齐格式的问题
16            auto padding = handle_padspec_(++it, end);
17
18            if (it != end) {
19                if (padding.enabled()) {
20                    // pattern 中指定了宽度,例如 %8l、%-8l、%=8l
21                    // 该函数进行基础的
22                    handle_flag_<details::scoped_padder>(*it, padding);
23                } else {
24                    // 没有指定宽度,例如 %l
25                    handle_flag_<details::null_scoped_padder>(*it, padding);
26                }
27            } else {
28                break;
29            }
30        } else  // chars not following the % sign should be displayed as is
31        {
32            // 对于普通的字符,就正常加到user_char里面
33            if (!user_chars) {
34                // 因为有move,所以有可能为空
35                user_chars = details::make_unique<details::aggregate_formatter>();
36            }
37            user_chars->add_ch(*it);
38        }
39    }
40    // 如果最后是纯文本,也应当处理
41    if (user_chars)  // append raw chars found so far
42    {
43        formatters_.push_back(std::move(user_chars));
44    }
45}

对于compile_pattern_,其核心思路是分割不同的规格说明,以[%n]为例,在经过上述函数后,会在formatters_中添加如下几个类型的flag_formatter:

  1. aggregate_formatter,对应[
  2. name_formatter,对应%n
  3. aggregate_formatter,对应] 这里由于篇幅原因,并不细讲handle_flag_的处理,但也比较简单。

对于这三个flag_formatter的作用暂时省略,回过头再看看pattern_formatter的format方法的实现:

1for (auto &f : formatters_) {
2    f->format(msg, cached_tm_, dest);
3}

显然是调用各个flag_format的format方法来实现的。 核心其实只有一个,把log_msg的东西按照规格说明写入到dest里。

flag_formatter

 1// pattern_formatter-inl.h
 2// 以logger的formatter为例
 3template <typename ScopedPadder>
 4class name_formatter final : public flag_formatter {
 5public:
 6    explicit name_formatter(padding_info padinfo)
 7        : flag_formatter(padinfo) {}
 8
 9    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest) override {
10        // 在这里构造scopedpadder,专门负责处理缩进之类的事情,这里依据缩进的大小将缩进处理好
11        ScopedPadder p(msg.logger_name.size(), padinfo_, dest);
12        // 这里填充具体的内容
13        fmt_helper::append_string_view(msg.logger_name, dest);
14        // 析构ScopedPadder时,负责后半部分的填充和截断
15    }
16};
17
18// 普通文本格式化器,原样直出
19// aggregate user chars to display as is
20class aggregate_formatter final : public flag_formatter {
21public:
22    aggregate_formatter() = default;
23
24    void add_ch(char ch) { str_ += ch; }
25    // 这里也能看出来
26    void format(const details::log_msg &, const std::tm &, memory_buf_t &dest) override {
27        fmt_helper::append_string_view(str_, dest);
28    }
29
30private:
31    std::string str_;
32};

可以看到,到flag_formatter这一层基本就完成了一条日志消息的输出。 当然这里省略了相当多的细节,例如padding、自定义pattern、色彩输出等等,但结构是足够完整的,我们可以看到一条层次分明的脉络,log_msg在各层之间传递,不同对象各司其职,每个层级都只做一件事情,并且边界清晰。

小结

相较于前一篇的log.c,spdlog无疑是一套更加出色、使用了良好工程实践的日志库,使用了相当多C++特性,没办法在一篇文章全部讲完。

我想合适的篇幅应当是在三篇文章左右,下一篇应当会谈谈spdlog是如何做到线程安全、异步等,以及使用了哪些值得一提的C++技术,比如模版、RingBuffer、单例之类,最后一篇会做几个小实验,来评估一下其性能优劣。