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才能填充。根据填充空格跟正文的位置关系,填充空格方式分三类:左侧填充、两侧填充、右侧填充。使用空格字符(
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规格,格式为
// 按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的宽度数字(
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
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(),不过函数类型是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库笔记汇总