spdlog日志库源码:formatter类

相关概念

spdlog并未说明何为scope(范围),何为pad(垫片)。在这里,我把要显示的一个固定宽度的pattern flag(形如"%8X" "%-8X" "%=8!X")称为scope,没有指定宽度的称为null-scope,进行填充动作的类称为padder(垫片器,填充器),包含填充信息的类为padding_info。一个pattern flag是一个模式标记,对应一个要显示的字段,当该字段的宽度固定时,如果固定宽度 > 实际显示宽度,就需要填充;如果固定宽度 < 实际显示宽度,就需要截断,即清空超出部分。

一个字段对应一个scope,一个scope需要一个padder才能进行填充动作,而一个padder需要计算出padding_info才能填充。根据填充空格跟正文的位置关系,填充空格方式分三类:左侧填充、两侧填充、右侧填充。使用空格字符()填充,填充只针对固定宽度的pattern flag,而且宽度必须为0~64。

formatter类描述

logger对象负责接收用户log消息,sink对象负责将log消息写到目标文件,但如何将用户传入的原始log消息,按期望格式转换为最终的一条完整的log呢,比如包含时间、日志等级、载荷等信息?
每个sink对象拥有一个formatter类对象的成员,用于将原始的log消息格式化为字符串。pattern(模式串)用于指定期望的格式,在formatter构造时传入。

formatter系列类类图关系

  • formatter及其子类负责对pattern进行解析,识别出的pattern flag交给对应的flag_formatter对象成员对log消息进行格式化(format);
  • 一个flag_formatter子类对应一种具体的pattern flag,对log消息进行格式化;
  • 格式化时,如果需要填充,就用padding_info类对象表示;
  • 如果需要自定义pattern flag,可继承custom_flag_formatter类,实现clone和format接口;

formatter类

formatter是一个接口类,规定派生类必须实现的接口。

format接口:格式化原始的log消息, 将转换结果存放到dest二进制缓存;
clone接口:克隆一个当前formatter类对象,注意clone的对象由unique_ptr管理,无需手动释放内存;

class formatter
{
public:
    virtual ~formatter() = default;
    // 格式化log消息, 结果存放到dest二进制缓存
    virtual void format(const details::log_msg &msg, memory_buf_t &dest) = 0;
    // 克隆对象
    virtual std::unique_ptr<formatter> clone() const = 0;
};

思考:为什么formatter类需要clone()?
因为用户为一个logger的sink成员定义了formatter对象,希望也能用到另一个sink上,那么此时最好的选择就是clone一个formatter对象。这样,不同sink对象就能共享相同的formatter(但不是同一个对象)。

pattern_formatter类

用户如何指定一条log消息最终输出内容?
可以通过pattern_formatter类,该类的作用是根据用户指定的pattern,来对log消息进行格式化,结果存放到二进制缓存。有2个关键点:1)解析用户pattern字符串(compile_pattern_);2)对log消息进行格式化(format)。

模式字符串pattern描述了最终log消息的格式,包含哪些内容以及其顺序。pattern是由若干普通字符串、模式标志(pattern flag)组成的格式字符串。模式标志以"%"开头(%flag),例如,%v表示实际要记录的文本,%t表示线程id,%P表示进程id,"%+"(缺省pattern)表示最终输出log消息应形如"[2022-10-31 23:46:59.678] [mylogger] [info] Some message"。

一个完整的pattern flag,包含宽度(width)、对齐方式(alignment)、截断(truncate)、模式标志(flag),语法格式如下:

%<alignment><width>!<flag>

每个模式标志可以通过宽度数字(0~64)实现对齐,用"-"(左对齐),"="(中对齐)控制对齐方式,缺省是右对齐;
通过"!"指定截断,截断必须配合宽度使用。如果指定了截断,当要显示字符数超过宽度时,超出部分就会忽略。

pattern_formatter声明式:

// 根据pattern对log消息进行格式化
class SPDLOG_API pattern_formatter final : public formatter
{
public:
    // 用户自定义模式标志, 每个字符对应一个自定义标志格式custom_flag_formatter对象, 每个custom_flag_formatter对应一个模式标志
    using custom_flags = std::unordered_map<char,  std::unique_ptr<custom_flag_formatter>>;
    
    // 由调用者决定pattern
    explicit pattern_formatter(std::string pattern, pattern_time_type time_type =  pattern_time_type::local,
        std::string eol = spdlog::details::os::default_eol, custom_flags  custom_user_flags = custom_flags());
    
    // 使用默认的pattern(%+)
    // default_eol 是行结束符, 两种风格: "\r\n"(Windows), "\n"(Unix)
    // use default pattern is not given
    explicit pattern_formatter(pattern_time_type time_type =  pattern_time_type::local, std::string eol = spdlog::details::os::default_eol);

    // 禁用copy操作
    pattern_formatter(const pattern_formatter &other) = delete;
    pattern_formatter &operator=(const pattern_formatter &other) = delete;

    // 实现父类pure virtual函数, 克隆对象
    std::unique_ptr<formatter> clone() const override;
    void format(const details::log_msg &msg, memory_buf_t &dest) override;

    // 添加模式标志flag
    template<typename T, typename... Args>
    pattern_formatter &add_flag(char flag, Args &&... args);

    // 设置模式串
    void set_pattern(std::string pattern);
    // 设置是否需要localtime (对应need_localtime_的值)
    void need_localtime(bool need = true);

private:
    std::string pattern_; // 模式串
    std::string eol_;     // 行结束符
    pattern_time_type pattern_time_type_; // 模式时间类型: local or utc time
    bool need_localtime_; // 决定用户可指定最终输出log消息的时间是使用本地时间, 还是使用log消息自带的时间(log_msg构造时决定)
    std::tm cached_tm_;   // 搭配need_localtime_使用, 用于缓存最终输出的log消息时间
    std::chrono::seconds last_log_secs_;  // 最近一次写log的时间(秒)
    std::vector<std::unique_ptr<details::flag_formatter>> formatters_; // 标志格式数组
    custom_flags custom_handlers_;        // 用户自定义模式标志

    std::tm get_time_(const details::log_msg &msg); // 获取当前时间, 由pattern_time_type_决定local time or utc time
    template<typename Padder>
    void handle_flag_(char flag, details::padding_info padding);      // 处理模式标志字符flag

    // 提取给定pad规范, 如%8X, 返回padding_info
    // Extract given pad spec (e.g. %8X)
    // Advance the given it pass the end of the padding spec found (if any)
    // Return padding.
    static details::padding_info handle_padspec_(std::string::const_iterator &it,  std::string::const_iterator end);
    
    // 编译pattern, 抽取出模式标志
    void compile_pattern_(const std::string &pattern);
};

pattern_time_type_与need_localtime_都可控制是否使用local time,有什么区别?
pattern_time_type_值类构造时决定,表示所用时区时间,值为local time或utc time;而need_localtime_是构造后再设置,表示最终log消息输出时间是用原始log消息自带的时间,还是输出log消息时的局部时间。就时区而言,need_localtime_表示的时间可以是local time,也可以是utc time,这由need_localtime_决定。

构造与析构

pattern_formatter定义了2个版本的构造函数:第一个用于用户指定模式串pattern的情形,第二个用于用户未指定的情形。当用户未指定pattern时,使用默认模式串"%+",输出log形如"[2022-10-31 23:46:59.678] [mylogger] [info] Some message"。

也就是说,构造时需要指定pattern,然后调用compile_pattern_对pattern进行“编译”(即解析)。

// 根据用户指定pattern构造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
}

// 使用默认的pattern构造pattern_formatter对象
// use by default full formatter for if pattern is not given
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_));
     // full_formatter对应pattern: [%Y-%m-%d %H:%M:%S.%e] [%n] [%l] [%s:%#] %v
     formatters_.push_back(details::make_unique<details::full_formatter>(details::padding_info{}));
}

compile_pattern_编译pattern

第一个版本构造器调用了compile_pattern_,而compile_pattern_主要工作是编译pattern中模式标志,即"%"开头的字符串。所谓编译pattern,指对pattern字符串进行解析,得到一组formatter对象。

下面我们看下工作过程:

// 编译用户pattern, 例如"%+", 
SPDLOG_INLINE void pattern_formatter::compile_pattern_(const std::string &pattern)
{
    auto end = pattern.end();
    // user_chars用于存放普通用户字符串
    std::unique_ptr<details::aggregate_formatter> user_chars;
    formatters_.clear();

    // 逐字符遍历pattern
    for (auto it = pattern.begin(); it != end; ++it)
    {
        // 对跟着"%"的字符串, 调用handle_padspec_按模式标志进行处理
        if (*it == '%')
        {
            // 附加到目前为止的普通用户字符
            if (user_chars) // append user chars found so far
            {
                formatters_.push_back(std::move(user_chars));
            }
            // 按pad规范处理模式标志, 得到padding_info, 需要指定<width>
            // 注意handle_padspec_会改变迭代器it
            auto padding = handle_padspec_(++it, end);
            if (it != end) // 此时it指向的模式标志 非空
            {
                if (padding.enabled()) // 有效的pad
                {
                    handle_flag_<details::scoped_padder>(*it, padding);
                }
                else // 无效的pad
                {
                    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);
        }
    }
    // 不要忘了最后一个"%"后的字符串
    if (user_chars) // append raw chars found so far
    {
        formatters_.push_back(std::move(user_chars));
    }
}

handle_padspec_按pad规格处理

参数迭代器it开始指向pattern中某个模式标志的"%"后的第一个字符,end指向最后一个。对于给定pad规格,格式为[!],如%8X, %=8X, %-8!X, %8!X, %=8!X, %-8!X, %+8!X,handle_padspec_就是逐字符解析。有一点需要注意:是必须的,因为没有指定宽度,就无所谓对齐、填充;否则,就返回空的padding_info。

// 按pad规格, 逐字符解析模式标志, 即%后的3段: <alignment><width>[!]<flag>
// handle_padspec_可能改变迭代器it, 正常时, 最终指向<flag>或者end; 或者中间出错, 如没有指定<width>

// Extract given pad spec (e.g. %8X, %=8X, %-8!X, %8!X, %=8!X, %-8!X, %+8!X)
// Advance the given it pass the end of the padding spec found (if any)
// Return padding.
SPDLOG_INLINE details::padding_info  pattern_formatter::handle_padspec_(std::string::const_iterator &it,  std::string::const_iterator end)
{
    using details::padding_info;
    using details::scoped_padder;
    const size_t max_width = 64;
    if (it == end) // 迭代器范围为空, 就返回空的padding_info
    {
        return padding_info{}; 
    }

    // 判断对齐方式<alignment>
    padding_info::pad_side side;
    switch (*it)
    {
    case '-': // 右对齐
        side = padding_info::pad_side::right;
        ++it;
        break;
    case '=': // 中对齐
        side = padding_info::pad_side::center;
        ++it;
        break;
    default: // 左对齐
        side = details::padding_info::pad_side::left;
        break;
    }

    // 非数字就返回空padding_info
    // FIXME: why?
    // 因为没有指定<width>, 就无所谓对齐, 也无所谓填充
    if (it == end || !std::isdigit(static_cast<unsigned char>(*it)))
    {
        return padding_info{}; // no padding if no digit found here
    }

    // 宽度<width>
    auto width = static_cast<size_t>(*it) - '0';
    // 将数字字符串转换为十进制数
    for (++it; it != end && std::isdigit(static_cast<unsigned char>(*it)); ++it)
    {
        auto digit = static_cast<size_t>(*it) - '0';
        width = width * 10 + digit;
    }

    // 截断标记[!]
    // search for the optional truncate marker '!'
    bool truncate;
    if (it != end && *it == '!')
    {
        truncate = true;
        ++it;
    }
    else
    {
        truncate = false;
    }

    // 此时it指向最后的<flag>
    // 返回当前pad对应的padding_info
    return details::padding_info{std::min<size_t>(width, max_width), side,  truncate};
}

handle_flag_ 处理模式标志字符

前面handle_padspec_处理了对齐、宽度、截断,这里handle_flag_只处理单个的模式标志字符。handle_flag_根据传入的模式标志字符flag、填充信息padding,来处理模式标志。pattern_formatter类会为每个模式标志都生成一个flag_formatter对象,存入formatters_数组中。handle_flag_就是根据不同的flag,生成不同类型的flag_formatter对象。

关于模板参数Padder,handle_flag_是转发给flag_formatter子类模板,在子类模板的接口format()实现中利用Padder的构造与析构,完成左侧、右侧空格填充。这种方法类似于RAII,代码会显得是否优雅,而且复用性高。

// Padder: 转发给formatter, 用类RAII方法填充空格的类型
// flag: 模式标志字符, 如"%-8!X" 中的'X'
// padding: 填充信息
template<typename Padder>
SPDLOG_INLINE void pattern_formatter::handle_flag_(char flag,  details::padding_info padding)
{
    // 处理用户自定义的flag, 会clone一个custom_flag_formatter对象, 加入formatters_数组
    // process custom flags
    auto it = custom_handlers_.find(flag);
    if (it != custom_handlers_.end())
    {
        auto custom_handler = it->second->clone(); // clone一个自定义flag_formatter对象(custom_flag_formatter类型)
        custom_handler->set_padding_info(padding); // 设置padding_info属性
        formatters_.push_back(std::move(custom_handler));
        return;
    }

    // 处理内置flag
    // process built-in flags
    switch (flag)
    {
    case ('+'): // default formatter
        // spdlog默认format, i.e. "[2022-10-31 23:46:59.678] [mylogger] [info] Some message"
         formatters_.push_back(details::make_unique<details::full_formatter>(padding));
        need_localtime_ = true;
        break;
    case 'n': // logger name
         formatters_.push_back(details::make_unique<details::name_formatter<Padder>>(padding)); // Padder转发给name_formatter
        break;
    case 'l': // level
         formatters_.push_back(details::make_unique<details::level_formatter<Padder>>(padding)); // Padder转发给level_formatter
        break;
    case 'L': // short level
         formatters_.push_back(details::make_unique<details::short_level_formatter<Padder>>(padding));
        break;
    case ('t'): // thread id
         formatters_.push_back(details::make_unique<details::t_formatter<Padder>>(padding));
        break;
    case ('v'): // the message text
         formatters_.push_back(details::make_unique<details::v_formatter<Padder>>(padding));
        break;
    case ('a'): // weekday
         formatters_.push_back(details::make_unique<details::a_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('A'): // short weekday
         formatters_.push_back(details::make_unique<details::A_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('b'):
    case ('h'): // month
         formatters_.push_back(details::make_unique<details::b_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('B'): // short month
         formatters_.push_back(details::make_unique<details::B_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('c'): // datetime
         formatters_.push_back(details::make_unique<details::c_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('C'): // year 2 digits
         formatters_.push_back(details::make_unique<details::C_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('Y'): // year 4 digits
         formatters_.push_back(details::make_unique<details::Y_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('D'):
    case ('x'): // datetime MM/DD/YY
         formatters_.push_back(details::make_unique<details::D_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('m'): // month 1-12
         formatters_.push_back(details::make_unique<details::m_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('d'): // day of month 1-31
         formatters_.push_back(details::make_unique<details::d_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('H'): // hours 24
         formatters_.push_back(details::make_unique<details::H_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('I'): // hours 12
         formatters_.push_back(details::make_unique<details::I_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('M'): // minutes
         formatters_.push_back(details::make_unique<details::M_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('S'): // seconds
         formatters_.push_back(details::make_unique<details::S_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('e'): // milliseconds
         formatters_.push_back(details::make_unique<details::e_formatter<Padder>>(padding));
        break;
    case ('f'): // microseconds
         formatters_.push_back(details::make_unique<details::f_formatter<Padder>>(padding));
        break;
    case ('F'): // nanoseconds
         formatters_.push_back(details::make_unique<details::F_formatter<Padder>>(padding));
        break;
    case ('E'): // seconds since epoch
         formatters_.push_back(details::make_unique<details::E_formatter<Padder>>(padding));
        break;
    case ('p'): // am/pm
         formatters_.push_back(details::make_unique<details::p_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('r'): // 12 hour clock 02:55:02 pm
         formatters_.push_back(details::make_unique<details::r_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('R'): // 24-hour HH:MM time
         formatters_.push_back(details::make_unique<details::R_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('T'):
    case ('X'): // ISO 8601 time format (HH:MM:SS)
         formatters_.push_back(details::make_unique<details::T_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('z'): // timezone
         formatters_.push_back(details::make_unique<details::z_formatter<Padder>>(padding));
        need_localtime_ = true;
        break;
    case ('P'): // pid
         formatters_.push_back(details::make_unique<details::pid_formatter<Padder>>(padding));
        break;
    case ('^'): // color range start
         formatters_.push_back(details::make_unique<details::color_start_formatter>(padding));
        break;
    case ('$'): // color range end
         formatters_.push_back(details::make_unique<details::color_stop_formatter>(padding));
        break;
    case ('@'): // source location (filename:filenumber)
         formatters_.push_back(details::make_unique<details::source_location_formatter<Padder>>(padding));
        break;
    case ('s'): // short source filename - without directory name
         formatters_.push_back(details::make_unique<details::short_filename_formatter<Padder>>(padding));
        break;
    case ('g'): // full source filename
         formatters_.push_back(details::make_unique<details::source_filename_formatter<Padder>>(padding));
        break;
    case ('#'): // source line number
         formatters_.push_back(details::make_unique<details::source_linenum_formatter<Padder>>(padding));
        break;
    case ('!'): // source funcname
         formatters_.push_back(details::make_unique<details::source_funcname_formatter<Padder>>(padding));
        break;
    case ('%'): // % char
        formatters_.push_back(details::make_unique<details::ch_formatter>('%'));
        break;
    case ('u'): // elapsed time since last log message in nanos
         formatters_.push_back(details::make_unique<details::elapsed_formatter<Padder,  std::chrono::nanoseconds>>(padding));
        break;
    case ('i'): // elapsed time since last log message in micros
         formatters_.push_back(details::make_unique<details::elapsed_formatter<Padder,  std::chrono::microseconds>>(padding));
        break;
    case ('o'): // elapsed time since last log message in millis
         formatters_.push_back(details::make_unique<details::elapsed_formatter<Padder,  std::chrono::milliseconds>>(padding));
        break;
    case ('O'): // elapsed time since last log message in seconds
         formatters_.push_back(details::make_unique<details::elapsed_formatter<Padder,  std::chrono::seconds>>(padding));
        break;

    default: // Unknown flag appears as is
        // 未定义flag当做普通字符串处理, 而aggregate_formatter用于处理普通字符串
        auto unknown_flag = details::make_unique<details::aggregate_formatter>();

        if (!padding.truncate_) // 未指定截断, 将%flag作为普通字符加入aggregate_formatter
        {
            unknown_flag->add_ch('%');
            unknown_flag->add_ch(flag);
            formatters_.push_back((std::move(unknown_flag)));
        }
        // fix issue #1617 (prev char was '!' and should have been treated as  funcname flag instead of truncating flag)
        // spdlog::set_pattern("[%10!] %v") => "[      main] some message"
        // spdlog::set_pattern("[%3!!] %v") => "[mai] some message"
        else // 指定了截断, 将截断标记'!'当函数名进行截断
        {
            padding.truncate_ = false;
            formatters_.push_back(details::make_unique<details::source_funcname_formatter<Padder>>(padding));
            unknown_flag->add_ch(flag);
            formatters_.push_back((std::move(unknown_flag)));
        }
        break;
    }
}

format()格式化log消息

构造函数中,会编译pattern,根据pattern生成一系列formatter子类,添加进formatters_数组,用于后面将原始的log消息根据pattern转换成最终的log消息。

转换工作由谁,何时以何种方式进行?
转换工作是交给pattern_formatter::format,然后转交给formatters_的每个成员的format函数,也就是对每个pattern flag进行转换,最终的结果都是存放到二进制内存memory_buf_t中。

SPDLOG_INLINE void pattern_formatter::format(const details::log_msg &msg,  memory_buf_t &dest)
{
    if (need_localtime_) // 使用局部时间, 这里并非值本地时间, 而是指调用format的时间
    {
        // 获取epoch时间, 单位: 秒
        const auto secs =  std::chrono::duration_cast<std::chrono::seconds>(msg.time.time_since_epoch());
        if (secs != last_log_secs_) // 如果不是同一秒, 就更新缓存时间cached_tm_
        {
            cached_tm_ = get_time_(msg); // 从msg获取log消息的时间
            last_log_secs_ = secs;
        }
    }

    // 对pattern flag逐个格式化, 得到的dest就是最终的log消息对应二进制缓存
    for (auto &f : formatters_)
    {
        f->format(msg, cached_tm_, dest);
    }
    // eol是附加到每条log消息末尾的字符串
    // write eol
    details::fmt_helper::append_string_view(eol_, dest);
}

可以从下面的get_time_实现看出,need_localtime_ 并非指本地时间,因为既可以返回localtime,也可返回gmtime(取决于pattern_time_type_),而是指调用format时间。

// 获取log消息对象的构造时间
// 根据pattern_time_type_(时间类型: local or utc), 来选择返回local time, or gmtime
SPDLOG_INLINE std::tm pattern_formatter::get_time_(const details::log_msg &msg)
{
    if (pattern_time_type_ == pattern_time_type::local)
    {
        return details::os::localtime(log_clock::to_time_t(msg.time));
    }
    return details::os::gmtime(log_clock::to_time_t(msg.time));
}

set_pattern更新pattern

前面已经知道,构造pattern_formatter对象时,必须指定pattern,否则使用默认的(%+)。如果想要更新绑定pattern_formatter的pattern,可以调用set_pattern。

// 设置pattern
SPDLOG_INLINE void pattern_formatter::set_pattern(std::string pattern)
{
    pattern_ = std::move(pattern);
    need_localtime_ = false;
    compile_pattern_(pattern_);
}

add_flag<T, ...Args>添加pattern flag

用户自定义的pattern flag,pattern_formatter如何知道呢?
可以通过pattern_formatter::add_flag,添加自定义pattern flag。不同于spdlog默认的pattern flag,自定义的pattern flag通过custom_handlers_(一个map)存储。

add_flag是一个包含变长模板函数,T是创建的对象类型,Args是模板参数包。

public:
    // T: 要创建的对象类型
    // Args: 模板参数包
    template<typename T, typename... Args>
    pattern_formatter &add_flag(char flag, Args &&... args) // args是变长类型的数据, 即函数参数包
    {
        // 利用变长数据构造T对象, 插入map类型的custom_handlers_, 索引是字符flag
        // 包扩展形如 arg1, arg2, ...
        // 每个参数都使用完美转发,保持实参的原始性质
        custom_handlers_[flag] =  details::make_unique<T>(std::forward<Args>(args)...); 
        return *this;
    }

clone克隆对象

如果想克隆当前对象,可以使用clone()接口。实现思路是利用现有数据成员,构造一个新的pattern_formatter对象,返回给调用者。

思考:pattern_formatter为何禁用copy操作,却允许clone操作?
因为copy操作(编译器合成)通常是浅拷贝,而clone通常是深拷贝。例如,formatters_成员是std::vector<std::unique_ptrdetails::flag_formatter>,如果使用默认合成的copy操作,就会导致vector元素的copy,而元素类型是unique_ptr不允许copy,因此可能会造成错误;如果是进行move操作,则会导致原来vecotr成员所有权转移,从而指向nullptr。

SPDLOG_INLINE std::unique_ptr<formatter> pattern_formatter::clone() const
{
    custom_flags cloned_custom_formatters;
    // 克隆自定义custom_handlers_ 自定义标记formatter的map
    for (auto &it : custom_handlers_)
    {
        cloned_custom_formatters[it.first] = it.second->clone();
    }

    // 构造一个新的pattern_formatter对象, 交给unique_ptr管理
    auto cloned = details::make_unique<pattern_formatter>(pattern_,  pattern_time_type_, eol_, std::move(cloned_custom_formatters));
    // 克隆need_localtime_属性
    cloned->need_localtime(need_localtime_);
    
    // 返回克隆对象
#if defined(__GNUC__) && __GNUC__ < 5
    return std::move(cloned);
#else
    return cloned;
#endif
}

formatter子类模板实参Padder

构造formatter子类对象时,有些子类需填充(用空格字符)格式转换后的二进制内存的多余空间。spdlog提供的方案,是使用模板参数Padder,而作为其实参有两套解决方案:scoped_padder类、null_scoped_padder类。前者是空格填充,后者是不进行实际填充。但为了保持代码的一致性,故而提供null_scoped_padder作为空对象模式。
这与线程安全的解决方案是类似的,提供空锁null_mutex用于非线程安全版本。

scoped_padder类

flag_formatter子类会在format()中利用scoped_padder,对固定宽度的字段填充空格。scoped_padder在构造函数中,填充该字段正文左侧空格,计算出右侧该填充空格数,待析构时填充右侧空格。这种方法十分精妙,利用对象的构造与析构,自动完成填充并且确保正文段与填充段的顺序,有点类似于RAII。

注意:scoped_padder用一个含64个空格字符数组来作为填充字符,也就意味着pad_it的参数count不能 > 64,这与spdlog要求pattern的宽度数字()为0~64相对应。

class scoped_padder
{
public:
    // 通过构造函数填充左侧
    // wrapped_size: 包裹字符串的长度(实际宽度)
    // padinfo: 填充信息(限制宽度)
    // dest: 二进制缓存(结果)
    scoped_padder(size_t wrapped_size, const padding_info &padinfo, memory_buf_t  &dest)
        : padinfo_(padinfo)
        , dest_(dest)
    {
        // 计算待填充长度
        remaining_pad_ = static_cast<long>(padinfo.width_) -  static_cast<long>(wrapped_size);
        if (remaining_pad_ <= 0)
        {
            return;
        }

        // 左侧填充空格, 正文右对齐
        if (padinfo_.side_ == padding_info::pad_side::left)
        {
            pad_it(remaining_pad_);
            remaining_pad_ = 0;
        }
        // 两侧填充空格, 正文中心对齐
        else if (padinfo_.side_ == padding_info::pad_side::center)
        {
            auto half_pad = remaining_pad_ / 2;
            auto reminder = remaining_pad_ & 1;
            pad_it(half_pad); // 左侧填充空格
            // 计算余下空格宽度, 用于右侧, 因为正文尚未填充, 所以暂时只是计算
            remaining_pad_ = half_pad + reminder; // for the right side
        }
    }
    
    // 计算n有多少位数
    template<typename T>
    static unsigned int count_digits(T n)
    {
        return fmt_helper::count_digits(n);
    }
    
    // 通过析构函数填充右侧, 如果需要填充的话
    ~scoped_padder()
    {
        if (remaining_pad_ >= 0)
        {
            pad_it(remaining_pad_); // 填充剩余位置
        }
        else if (padinfo_.truncate_) // 没有需要填充的, 就看是否需要截断
        {
            // 重新计算缓存dest大小
            long new_size = static_cast<long>(dest_.size()) + remaining_pad_;
            dest_.resize(static_cast<size_t>(new_size)); 
        }
    }

private:
    // 填充count个空格字符
    void pad_it(long count)
    {
        fmt_helper::append_string_view(string_view_t(spaces_.data(),  static_cast<size_t>(count)), dest_);
    }

    const padding_info &padinfo_;  // 填充信息
    memory_buf_t &dest_;           // 结果缓存
    long remaining_pad_;           // 待填充pad长度
    string_view_t spaces_{"                                                                 ", 64}; // 64个空格字符的数组
};

count_digits计算n的位数

count_digits是定义在fmt_helper中的辅助函数,用于计算一个十进制数的位数。该函数可以用来计算一个整型数占用的字符数。在计算一个字段实际占用宽度时,十分有用。

// 函数模板
// 计算T类型的数字n的位数
template<typename T>
inline unsigned int count_digits(T n)
{
    // 自适应32bit/64bit平台
    using count_type = typename std::conditional<(sizeof(T) > sizeof(uint32_t)),  uint64_t, uint32_t>::type;
#ifdef SPDLOG_USE_STD_FORMAT
    return count_digits_fallback(static_cast<count_type>(n));
#else
    return static_cast<unsigned int>(fmt::
// fmt 7.0.0 renamed the internal namespace to detail.
// See: https://github.com/fmtlib/fmt/issues/1538
#    if FMT_VERSION < 70000
            internal
#    else
            detail
#    endif
        ::count_digits(static_cast<count_type>(n)));
#endif
}

// 函数模板特化

// n针对64bit平台特化
// Returns the number of decimal digits in n. Leading zeros are not counted
// except for n == 0 in which case count_digits returns 1.
FMT_CONSTEXPR20 inline auto count_digits(uint64_t n) -> int {
#ifdef FMT_BUILTIN_CLZLL
  if (!is_constant_evaluated()) {
    return do_count_digits(n);
  }
#endif
  return count_digits_fallback(n);
}

// n针对32bit平台特化
// Optional version of count_digits for better performance on 32-bit platforms.
FMT_CONSTEXPR20 inline auto count_digits(uint32_t n) -> int {
#ifdef FMT_BUILTIN_CLZ
  if (!is_constant_evaluated()) {
    return do_count_digits(n);
  }
#endif
  return count_digits_fallback(n);
}

// 计算n位数的核心算法, 以10000为单位进行判断, 比循环/10更快
template <typename T> FMT_CONSTEXPR auto count_digits_fallback(T n) -> int {
  int count = 1;
  for (;;) {
    // Integer division is slow so do it for a group of four digits instead
    // of for every digit. The idea comes from the talk by Alexandrescu
    // "Three Optimization Tips for C++". See speed-test for a comparison.
    if (n < 10) return count;
    if (n < 100) return count + 1;
    if (n < 1000) return count + 2;
    if (n < 10000) return count + 3;
    n /= 10000u;
    count += 4;
  }
}

null_scoped_padder类

假设我们没有为pattern flag指定宽度,该字段不需要填充,该怎么办?
为了不大幅改变代码,提高复用率,spdlog选择实现一个空的scoped_padder作为需要padder作为模板参数的场景,即null_scoped_padder。该类构造为空,析构函数也是默认的,这样就不进行任何填充。

null_scoped_padder/scoped_padder 都必须实现的public函数:构造函数、析构函数、count_digits

// 因为模板参数不能为空, spdlog选择实现空的scoped_padder
struct null_scoped_padder
{
    // 空的构造函数
    null_scoped_padder(size_t /*wrapped_size*/, const padding_info & /*padinfo*/,  memory_buf_t & /*dest*/) {}
    // 默认析构函数

    // 计算整型数的位数
    template<typename T>
    static unsigned int count_digits(T /* number */);
};

空的count_digits

直接返回0,并不对参数占用宽度进行计算。

public:
    static unsigned int count_digits(T /* number */)
    {
        return 0;
    }

flag_formatter类

对于像"%X"这样的pattern flag,谁来将其转换为应该呈现的内容及格式呢?
可以使用标志格式器(flag_formatter)。这个类实际上只是一个接口类,利用不同子类将原始log消息进行格式化,按所需的内容、格式,转换为最终的log消息。具体来说,通过format接口,格式化log消息。由于大多子类可能需要进行填充工作,为了便捷,flag_formatter直接包含padinfo_成员代表需要填充的信息。

// 一种flag_formatter子类, 对应一种flag的格式化
class SPDLOG_API flag_formatter
{
public:
    explicit flag_formatter(padding_info padinfo)
        : padinfo_(padinfo)
    {}
    flag_formatter() = default;
    virtual ~flag_formatter() = default;
    // flag_formatter并不负责具体pattern flag格式化, 交由子类进行, 因此为pure virtual函数
    // 格式化log消息msg, 结果存放到dest
    virtual void format(const details::log_msg &msg, const std::tm &tm_time,  memory_buf_t &dest) = 0;

protected:
    padding_info padinfo_;  // 空位填充信息
};

padding_info类

所谓padding是指:在一个固定总长度的内存区域中,正文(text)可以位于该区域的右侧、中间、左侧,分别对应正文右对齐、正文中对齐、正文左对齐,而剩余的部分可以用“0”填充(zero padding)。而padding_info类,就是用来携带这些信息的类。

padding_info声明式:

// padding information.
struct padding_info
{
    // 填充侧, 即表示填充的空位在哪一边
    enum class pad_side
    {
        left,
        right,
        center
    };

    // 构造一个空的对象
    padding_info() = default;
    // 构造一个包含填充信息的对象
    padding_info(size_t width, padding_info::pad_side side, bool truncate)
        : width_(width)
        , side_(side)
        , truncate_(truncate)
        , enabled_(true)
    {}

    // 可通过enabled()判断是否包含有意义的填充信息
    bool enabled() const
    {
        return enabled_;
    }

    size_t width_ = 0; // 区域的宽度
    pad_side side_ = pad_side::left; // 空位的位置
    bool truncate_ = false; // 是否截断
    bool enabled_ = false;  // 是否使能
};

aggregate_formatter子类

显示原始字符串或者未知flag,对应普通字符串或者未定义pattern flag:%flag。

aggregate_formatter会逐字符添加到str_进行存储。适用于逐字符解析pattern场景。例如,在构造pattern_formatter对象时,对pattern进行解析,将未定义%flag中的flag用一个aggreate_formatter对象存储,当需要format(格式化)时,就转发给aggreate_formatter::format()。

// 将用户字符汇总并直接展示
// aggregate user chars to display as is
class aggregate_formatter final : public flag_formatter
{
public:
    aggregate_formatter() = default;
    // 逐个添加用户字符ch
    void add_ch(char ch)
    {
        str_ += ch;
    }
    // 格式化就是把收集的用户字符附加到dest末尾
    void format(const details::log_msg &, const std::tm &, memory_buf_t &dest)  override
    {
        fmt_helper::append_string_view(str_, dest); // 将字符串str_ 附加到dest末尾
    }

private:
    // 存放普通字符串
    std::string str_;
};

// 客户端
// Unknow flag appears as is
auto unknown_flag = details::make_unique<details::aggregate_formatter>();
if (!padding.truncate_)
{
    unknown_flag->add_ch('%');
    unknown_flag->add_ch(flag);
    formatters_.push_back((std::move(unknown_flag)));
}
// fix issue #1617 (prev char was '!' and should have been treated as funcname  flag instead of truncating flag)
// spdlog::set_pattern("[%10!] %v") => "[      main] some message"
// spdlog::set_pattern("[%3!!] %v") => "[mai] some message"
else
{
    padding.truncate_ = false;
    formatters_.push_back(details::make_unique<details::source_funcname_formatter<Padder>>(padding));
    unknown_flag->add_ch(flag);
    formatters_.push_back((std::move(unknown_flag)));
}

fmt_helper::append_string_view是spdlog为fmt定义的辅助函数,用来将view字符串视图内容附加到缓存dest。

inline void append_string_view(spdlog::string_view_t view, memory_buf_t &dest)
{
    auto *buf_ptr = view.data();
    dest.append(buf_ptr, buf_ptr + view.size());
}

full_formatter子类

显示完整信息,对应pattern: %+ 或者 [%Y-%m-%d %H:%M:%S.%e] [%n] [%l] [%s:%#] %v
查阅用户手册的模式标志章节,可知,"[%Y-%m-%d %H:%M:%S.%e]" 时间格式,"[%n]" logger name,"[%l]" log level,"[%s:%#]" 源文件基础名+源代码的行数,"%v" 要记录的实际文本。
full_formatter也是默认pattern("%+")对应的formatter,因此pattern_formatter第二个版本构造函数会用到full_formatter。

// Full info formatter
// pattern: [%Y-%m-%d %H:%M:%S.%e] [%n] [%l] [%s:%#] %v
class full_formatter final : public flag_formatter
{
public:
    explicit full_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}
    
    // 负责对原始log消息格式化, 结果存放到缓存dest
    void format(const details::log_msg &msg, const std::tm &tm_time, memory_buf_t  &dest) override
    {
        using std::chrono::duration_cast;
        using std::chrono::milliseconds;
        using std::chrono::seconds;

        // time_since_epoch()是time_point成员函数, 用于求epoch时间, 参见time(2)
        // 从msg取得epoch时间
        // cache the date/time part for the next second.
        auto duration = msg.time.time_since_epoch();
        auto secs = duration_cast<seconds>(duration);

        // 下面条件必为true, 因为, 
        // cache_timestamp_初值0, secs通常非0, 
        // cached_datetime_初值空, 其size()为0
        if (cache_timestamp_ != secs || cached_datetime_.size() == 0)
        {
            cached_datetime_.clear();
            cached_datetime_.push_back('[');
            // 将第一个参数转换成字符串附加到第二个参数cached_datetime_(二进制缓存)末尾
            // 添加年份
            fmt_helper::append_int(tm_time.tm_year + 1900, cached_datetime_);
            cached_datetime_.push_back('-');

            // pad2类同append_int, 不过只会添加2位数的数字(0~99)
            // 添加月份
            fmt_helper::pad2(tm_time.tm_mon + 1, cached_datetime_);
            cached_datetime_.push_back('-');
            
            // 添加天数
            fmt_helper::pad2(tm_time.tm_mday, cached_datetime_);
            cached_datetime_.push_back(' ');

            // 添加小时
            fmt_helper::pad2(tm_time.tm_hour, cached_datetime_);
            cached_datetime_.push_back(':');

            // 添加分钟
            fmt_helper::pad2(tm_time.tm_min, cached_datetime_);
            cached_datetime_.push_back(':');

            // 添加秒
            fmt_helper::pad2(tm_time.tm_sec, cached_datetime_);
            cached_datetime_.push_back('.');
            cache_timestamp_ = secs;
        }
        // 把日期时间缓存加入目标缓存dest
        dest.append(cached_datetime_.begin(), cached_datetime_.end());

        // 从msg取得毫秒
        auto millis = fmt_helper::time_fraction<milliseconds>(msg.time);
        // 添加毫秒, pad3添加3位数的数字
        fmt_helper::pad3(static_cast<uint32_t>(millis.count()), dest);
        dest.push_back(']');
        dest.push_back(' ');
        
        // 添加log name
        // append logger name if exists
        if (msg.logger_name.size() > 0)
        {
            dest.push_back('[');
            fmt_helper::append_string_view(msg.logger_name, dest);
            dest.push_back(']');
            dest.push_back(' ');
        }

        dest.push_back('[');

        // 设置color范围为level name, 添加msg的level
        // level::to_string_view是将level转换为字符串类型
        // wrap the level name with color
        msg.color_range_start = dest.size();
        // fmt_helper::append_string_view(level::to_c_str(msg.level), dest);
        fmt_helper::append_string_view(level::to_string_view(msg.level), dest);
        msg.color_range_end = dest.size();
        dest.push_back(']');
        dest.push_back(' ');
        
        // 添加源文件名
        // add source location if present
        if (!msg.source.empty())
        {
            dest.push_back('[');
            const char *filename =  details::short_filename_formatter<details::null_scoped_padder>::basename(msg.source.filename);
            fmt_helper::append_string_view(filename, dest);
            dest.push_back(':');
            fmt_helper::append_int(msg.source.line, dest);
            dest.push_back(']');
            dest.push_back(' ');
        }

        // 添加正文
        // fmt_helper::append_string_view(msg.msg(), dest);
        fmt_helper::append_string_view(msg.payload, dest);
    }

private:
    std::chrono::seconds cache_timestamp_{0}; // 缓存的时间戳
    memory_buf_t cached_datetime_;            // 日期时间缓存
};

name_formatter子类

显示logger name,对应pattern flag:%n。

为何name_formatter是一个类模板,而不是一个普通类?
那就看模板参数有何用途:Padder可以用来填充多余部分,其他需要填充的patten flag也是用这种方法解决该问题的。好处是代码复用性很强,使用不同模板实参就能实现不同方案:填充,不填充。具体方法可以看下面format代码。

如果没有指定宽度,模板实参为scoped_padder, 会填充空格; 如果未指定时或出错时, 模板实参为null_scoped_padder, 不会真实的填充。

// logger name appender
template<typename ScopedPadder>
class name_formatter final : public flag_formatter
{
public:
    explicit name_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    // 用模板参数ScopedPadder构造一个对象,利用构造函数、析构函数,自动在fmt_helper::append_string_view前后进行填充
    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest)  override
    {
        ScopedPadder p(msg.logger_name.size(), padinfo_, dest); // 构造函数填充左侧
        fmt_helper::append_string_view(msg.logger_name, dest);  // 添加正文
        // 隐含的ScopedPadder析构函数(由编译器自动添加)填充右侧
    }
};

// 客户端
// 实例化类模板, 并构造一个name_formatter对象, 统一存入pattern_formatter::formatters_
// 当指定宽度时, 模板实参为scoped_padder, 会填充; 未指定时或出错时, 模板实参为null_scoped_padder, 不会真实填充
formatters_.push_back(details::make_unique<details::name_formatter<Padder>>(padding));

level_formatter子类

显示log level,对应pattern flag:%l

与name_formatter的区别是,添加正文前需要将log level转换成对应log level的名称(字符串类型)。

// log level appender
template<typename ScopedPadder>
class level_formatter final : public flag_formatter
{
public:
    explicit level_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest)  override
    {
        const string_view_t &level_name = level::to_string_view(msg.level); // 将log level转换为字符串
        ScopedPadder p(level_name.size(), padinfo_, dest); // 构造函数 填充左侧
        fmt_helper::append_string_view(level_name, dest);  // 添加正文
        // 析构函数 填充右侧
    }
};

// 客户端
formatters_.push_back(details::make_unique<details::level_formatter<Padder>>(padding));

short_level_formatter子类

显示简短的log level,对应pattern flag:%L

// short log level appender
template<typename ScopedPadder>
class short_level_formatter final : public flag_formatter
{
public:
    explicit short_level_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest)  override
    {
        string_view_t level_name{level::to_short_c_str(msg.level)}; // 将log level转换为简短字符串
        ScopedPadder p(level_name.size(), padinfo_, dest); // 构造函数 填充左侧
        fmt_helper::append_string_view(level_name, dest);  // 添加正文
        // 析构函数 填充右侧
    }
};

// 客户端
formatters_.push_back(details::make_unique<details::short_level_formatter<Padder>>(padding));

t_formatter子类

显示线程id,对应pattern flag:%t

// Thread id
template<typename ScopedPadder>
class t_formatter final : public flag_formatter
{
public:
    explicit t_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest)  override
    {
        // thread_id是整型数, 需要用ScopedPadder::count_digitis算出位数, 从而便于填充
        const auto field_size = ScopedPadder::count_digits(msg.thread_id);
        ScopedPadder p(field_size, padinfo_, dest);
        fmt_helper::append_int(msg.thread_id, dest);
    }
};

// 客户端
formatters_.push_back(details::make_unique<details::t_formatter<Padder>>(padding));

v_formatter子类

显示消息文本(用户希望记录的message text),对应pattern flag:%v

// message text
template<typename ScopedPadder>
class v_formatter final : public flag_formatter
{
public:
    explicit v_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest)  override
    {
        ScopedPadder p(msg.payload.size(), padinfo_, dest);
        // 载荷payload由用户通过日志库接口传入, 是用户希望记录的内容
        fmt_helper::append_string_view(msg.payload, dest);
    }
};

// 客户端
formatters_.push_back(details::make_unique<details::v_formatter<Padder>>(padding));

a_formatter子类

显示简写的星期几,对应pattern flag:%a

获取字符串类型的星期几的思路很简单,就是一个字符串数组包含一周7天星期几的简写。在格式化接口format实现中,将数值型的星期几通过数组索引转换成字符串类型。

// Abbreviated weekday name
static std::array<const char *, 7> days{{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri",  "Sat"}};

template<typename ScopedPadder>
class a_formatter final : public flag_formatter
{
public:
    explicit a_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    void format(const details::log_msg &, const std::tm &tm_time, memory_buf_t  &dest) override
    {
        string_view_t field_value{days[static_cast<size_t>(tm_time.tm_wday)]}; // 将星期几转换成字符串
        ScopedPadder p(field_value.size(), padinfo_, dest);
        fmt_helper::append_string_view(field_value, dest);
    }
};

// 客户端
formatters_.push_back(details::make_unique<details::a_formatter<Padder>>(padding));

A_formatter子类

显示星期几全称,对应pattern flag:%A

实现思路与a_formatter几乎完全一样,除了字符串数组表示星期几全名。

// Full weekday name
static std::array<const char *, 7> full_days{{"Sunday", "Monday", "Tuesday",  "Wednesday", "Thursday", "Friday", "Saturday"}};
template<typename ScopedPadder>
class A_formatter : public flag_formatter
{
public:
    explicit A_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    void format(const details::log_msg &, const std::tm &tm_time, memory_buf_t  &dest) override
    {
        string_view_t field_value{full_days[static_cast<size_t>(tm_time.tm_wday)]};
        ScopedPadder p(field_value.size(), padinfo_, dest);
        fmt_helper::append_string_view(field_value, dest);
    }
};

// 客户端
formatters_.push_back(details::make_unique<details::A_formatter<Padder>>(padding));

z_formatter子类

显示IOS 8601 规格时区偏移,例如"+02:00",对应pattern flag:%z

z_formatter的format实现思路很简单:计算时间差,得到符合ISO 8601规格的偏移时区时间。具体实现细节上,有些麻烦,因为涉及到多个时间。

  • msg.time: 用户记录log的时间,构造log_msg对象的时间,类型system_clock::time_point;
  • tm_time:调用formatter::format的时间,由调用者pattern_formatter传入,该时间分为两种情况:

1)pattern_formatter::need_localtime_为true,代表需要使用局部时间cached_tm_,即调用need_localtime_::format的时间;
2)pattern_formatter::need_localtime_为false,代表不需要使用局部时间cached_tm_,默认0。

// ISO 8601 offset from UTC in timezone (+-HH:MM)
template<typename ScopedPadder>
class z_formatter final : public flag_formatter
{
public:
    explicit z_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}
    
    z_formatter() = default;

    // 思考: 为什么z_formatter会禁用copy操作?
    z_formatter(const z_formatter &) = delete;
    z_formatter &operator=(const z_formatter &) = delete;

    void format(const details::log_msg &msg, const std::tm &tm_time, memory_buf_t  &dest) override
    {
        const size_t field_size = 6;
        ScopedPadder p(field_size, padinfo_, dest);
        
        // 计算log消息中时间点与tm_time代表的时间点的差
        auto total_minutes = get_cached_offset(msg, tm_time);
        // 负的时间差代表在标准时区之前, 用'-'表示; 正的代表在之后, 用'+'表示
        bool is_negative = total_minutes < 0;
        if (is_negative)
        {
            total_minutes = -total_minutes;
            dest.push_back('-');
        }
        else
        {
            dest.push_back('+');
        }

        // 以HH:MM 形式显示时间差
        fmt_helper::pad2(total_minutes / 60, dest); // hours
        dest.push_back(':');
        fmt_helper::pad2(total_minutes % 60, dest); // minutes
    }

private:
    log_clock::time_point last_update_{std::chrono::seconds(0)}; // 
    int offset_minutes_{0};

    int get_cached_offset(const log_msg &msg, const std::tm &tm_time)
    {
        // 每10秒才计算一次时间差
        // msg.time是log消息对象构造的时间点, 也就是用户记录log时间
        // last_update_是上一次更新log message的时间点, 由本函数负责更新
        // refresh every 10 seconds
        if (msg.time - last_update_ >= std::chrono::seconds(10))
        {
            // 计算本地时间tm_time与当前时间(gmt)的时间差, 以分钟形式返回差值
            offset_minutes_ = os::utc_minutes_offset(tm_time);
            last_update_ = msg.time; // 最近一次更新log message时间点
        }
        return offset_minutes_;
    }
};

关于last_update_,这里似乎存在一个问题:last_update_只有重复多次用到时,才会得到上一次更新log消息的时间到本次的时间差。但如果每次都是新构造的z_formatter对象,last_update_的初值始终为chrono::seconds(0),也就是起点未变过。实际上,z_formatter本身,也是每次调用pattern_formatter::format的时候创建并移交给pattern_formatter::formatters_持有的对象。

color_start_formatter与color_stop_formatter子类

color_start_formatter设置着色范围起点,对应pattern flag:%^
color_range_end设置着色范围终点,对应pattern flag:%$

color_start_formatter,color_range_end设置对log消息着色范围(color_range_start, color_range_end),会在对log消息进行着色的sink子类(wincolor_sink<> for Windows, or ansicolor_sink<> for non-Windows)中用到。

// color range start
// mark the color range. expect it to be in the form of "%^colored text%$"
class color_start_formatter final : public flag_formatter
{
public:
    explicit color_start_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest)  override
    {
        msg.color_range_start = dest.size();
    }
};

// color range end
class color_stop_formatter final : public flag_formatter
{
public:
    explicit color_stop_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest)  override
    {
        msg.color_range_end = dest.size();
    }
};

source_location_formatter子类

显示源文件和行号,例如"/some/dir/my_file.cpp:123",对应pattern flag:%@
源文件和行号来自于log消息msg。

// print source location
template<typename ScopedPadder>
class source_location_formatter final : public flag_formatter
{
public:
    explicit source_location_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}

    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest)  override
    {
        // 异常处理: 如果源文件信息为空
        if (msg.source.empty())
        {
            // 我们知道ScopedPadder构造函数会自动填充左侧, 析构函数填充右侧
            // 注意:同一个作用域内,不要同时构造2个ScopedPadder对象
            ScopedPadder p(0, padinfo_, dest);
            return;
        }

        size_t text_size;
        if (padinfo_.enabled())
        {
            // 计算文本实际长度
            // 为何不用strlen()计算源文件名长度?
            // std::char_traits<char>::length() 本质上调用的strlen, 不过编译器就能得到值, 更安全
            // +1 是为':'预留
            // calc text size for padding based on "filename:line"
            text_size = std::char_traits<char>::length(msg.source.filename) +  ScopedPadder::count_digits(msg.source.line) + 1;
        }
        else
        {
            text_size = 0;
        }
        
        // 填充左侧空格、正文、右侧空格
        // 正文为:源文件名:行号
        ScopedPadder p(text_size, padinfo_, dest);
        fmt_helper::append_string_view(msg.source.filename, dest);
        dest.push_back(':');
        fmt_helper::append_int(msg.source.line, dest);
    }
};

// 客户端
formatters_.push_back(details::make_unique<details::source_location_formatter<Padder>>(padding));

思考:计算源文件名长度时,为何使用std::char_traits::length,而不用strlen()?
length()本质上也是调用的strlen(),不过函数类型是inline(C++14)/constexpr(C++17),便于编译器得到结果,更加安全、快速。

ch_formatter子类

显示'%',对应pattern flag:%%

如果想显示'%'本身,可以使用ch_formatter。不像其他flag_formatter子类,构造函数不需要初始化父类padding_info_成员,即默认初始化为空,也就不需要填充。如此以来,不需要构造ScopedPadder,也就不需要模板参数。因此,ch_formatter不用实现为类模板。

// % char
class ch_formatter final : public flag_formatter
{
public:
    explicit ch_formatter(char ch)
        : ch_(ch)
    {}
    
    void format(const details::log_msg &, const std::tm &, memory_buf_t &dest)  override
    {
        dest.push_back(ch_);
    }
    
private:
    char ch_;
};

// 客户端
formatters_.push_back(details::make_unique<details::ch_formatter>('%'));

小结

类模板 + 空对象模式 实现两套解决方案

在flag_formatter子类中,通过模板参数Padder的构造函数、析构函数,来实现固定宽度的剩余空间(左侧、右侧)的填充。但有时候,我们没有指定字段宽度,也就是不需要填充。那么,如何让Padder客户端代码不变的情况下,不进行真实填充呢?
spdlog的解决方案是使用scoped_padder类、null_socped_padder类,作为模板实参。前者是会实际填充的类,后者是什么也不做的类,其对象是空对象。

类似的解决方案还有null_mutex、null_atomic_int。

// 客户端 只需要根据需要决定传入scoped_padder or null_scoped_padder
// 要填充, 模板实参为实际要填充的scoped_padder类
handle_flag_<details::scoped_padder>(*it, padding);

// 不填充, 模板实参为实际不填充的null_scoped_padder类
handle_flag_<details::null_scoped_padder>(*it, padding);

而scoped_padder/null_scoped_padder类型,最终会传递给flag_formatter子类的ScopedPadder,于是在flag_formatter子类的format实现中,利用ScopedPadder的构造与析构,完成空格填充。

// scoped_padder/null_scoped_padder类型 最终会传递给flag_formatter子类的ScopedPadder
// 例如name_formatter
template<typename ScopedPadder>
class name_formatter final : public flag_formatter
{
public:
    explicit name_formatter(padding_info padinfo)
        : flag_formatter(padinfo)
    {}
    
    // 格式化log消息
    void format(const details::log_msg &msg, const std::tm &, memory_buf_t &dest)  override
    {
        // 构造一个ScopedPadder对象, 利用构造函数对logger name字段(左侧)进行填充
        ScopedPadder p(msg.logger_name.size(), padinfo_, dest);

        fmt_helper::append_string_view(msg.logger_name, dest);

        // ScopedPadder析构函数(编译期自动添加), 利用析构函数对logger name字段(右侧)进行填充
    }
};

这种方式的好处是,客户端的代码几乎不用做任何改变,只需要实现两种scoped_padder类,并且根据需要传递给不同的flag_formatter子类即可。复用性极强,而且增加一套方案的支持代码改动量很小:只需要判断何时使用哪种方案。

构造函数简化代码

上面例子中,我们用模板参数ScopedPadder的构造函数、析构函数,来完成对某字段的填充。我们只需要在函数对local作用域,构造一个临时对象即可,析构函数是编译器自动加入的。代码看起来十分简洁:

void format()
{
    ScopedPadder p(...);
    ...
}

如果不这样做,我们可能需要这样:

void format()
{
    ScopedPadder p(...);
    p.fill_left(...);
    ...
    p.fill_right(...);
}

spdlog库系列:spdlog库笔记汇总

posted @ 2022-11-19 21:18  明明1109  阅读(1748)  评论(0编辑  收藏  举报