本文对sylar日志系统的日志初始化init方法进行解析,记录个人学习过程中的理解,不对的地方请多多指正。
新建一个日志类Logger的对象时,需要对其进行初始化,调用构造函数,如下:
// 构造函数
Logger::Logger(const std::string &name): m_name(name), m_level(LogLevel::DEBUG)
{
//将Logger中成员变量m_formatter进行初始化,指定了默认的日志格式(后面这一长串)
m_formatter.reset(new LogFormatter("%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
}
经过上边这个操作,我们的日志类Logger,它可以按照指定格式输出日志了,每个字符代表含义具体如下:
/**
* %m 消息
* %p 日志级别
* %r 累计毫秒数
* %c 日志名称
* %t 线程id
* %n 换行
* %d 时间
* %f 文件名
* %l 行号
* %T 制表符
* %F 协程id
* %N 线程名称
*/
例如指定上边格式后,日志输出结果为:
2023-11-07 10:06:00 2048 thread_name 1024 [INFO] [logger] /root/c_plus_plus_project/sylar/tests/test_log.cc:40 test info
// 时间 线程id 线程名称 协程id [日志级别] [日志名称] 文件名:行号: 消息 换行符号
那么具体如何从给定格式然后进行解析输出上边格式的日志呢?
核心就在于初始化方法,解析日志格式:
void LogFormatter::init();
我们先假设函数中字符串m_pattern值如下:
m_pattern="%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n";
我们先抛开代码不看,先看看如果让我们自己手动解析,应该如何解析?(先想清楚逻辑,再看代码,会比较清楚)
首先应该将m_pattern进行拆分,拆分成各个单项,然后将每个单项交给对应的解析类,调用对应解析类的fotmat方法,将解析结果存入到输出流中,这样所有单项解析完成后,输出流中就保存了我们想输出的日志。
m_pattern拆分结果如下:
m_pattern="%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n";
/*
%d -> %Y-%m-%d %H:%M:%S 说明:%d表示格式为时间,后面的大括号{}里面存放的是解析时间的格式,具体交给解析时间的类来处理。
%T
%t
%T
%N
%T
%F
%T
[
%p
]
%T
[
%c
]
%T
%f
:
%l
%T
%m
%n
*
这时候可能很多同学就有疑问了。
问:为什么大括号{}内的字符"%Y-%m-%d %H:%M:%S"没有被拆分?
答:前面%d表示格式为时间,后面的大括号{}里面存放的是解析时间的格式,具体交给解析时间的类来处理。函数LogFormatter::init()只需要找出大括号的起始位置’{‘和结束位置’}',然后把中间的内容%Y-%m-%d %H:%M:%S取出来,交给对应的解析类即可,例如时间,只需要将%Y-%m-%d %H:%M:%S交给时间解析类DateTimeFormatItem,告诉该如何解析时间**。
问:为什么字符 ‘[’,‘]’,‘:’ 被解析成了单行了,而不是写成[%p],[%c],%f: ?
答:我感觉日志解析类要求格式都应该以百分号’%‘开头,然后把其他字符 ‘[’,’]‘,’:’ ,都当作字符串来处理,对应解析类为StringFormatItem,然后直接输出’[‘,’]‘,’:’ 本身对应的值,(后面还会提到如何处理的)。
OK,现在我们似乎更够看出来一个套路,我们需要将m_pattern拆分成各个单项,然后找到单项对应的格式,然后给这个单项指定解析类。
可以写成如下的形式:
/*
<符号,格式,解析类>
举例:
<"p","",LevelFormatItem>
<"c","",NameFormatItem>
<"n","",NewLineFormatItem>
<"d","%Y-%m-%d %H:%M:%S",DateTimeFormatItem>
*/
另外,需要注意的是,本函数中新增了一位类型,来表示该格式是已经定义好的格式(例如%p,%c,…),还是普通字符格式(‘[’,‘]’,‘:’),普通字符格式需要使用StringFormatItem来进行处理。
上边的形式可改写如下:
/*
type为0,表示为普通字符,使用StringFormatItem来处理
type为1,表示为一定定义好的,使用对应的xxxFormatItem来处理
四元组
<符号,格式,解析类,type>
举例:
<"p","",LevelFormatItem,1>
<"c","",NameFormatItem,1>
<"n","",NewLineFormatItem,1>
<"d","%Y-%m-%d %H:%M:%S",DateTimeFormatItem,1>
<"[",StringFormatItem,0>
<"]",StringFormatItem,0>
<":",StringFormatItem,0>
*/
具体的符号和解析类的对应关系我放到了下面:
/**
* 符号 代表含义 解析类 解析格式
* %m 消息 MessageFormatItem -
* %p 日志级别 LevelFormatItem -
* %r 累计毫秒数 ElapseFormatItem -
* %c 日志名称 NameFormatItem -
* %t 线程id ThreadIdFormatItem -
* %n 换行 NewLineFormatItem -
* %d 时间 DateTimeFormatItem %Y-%m-%d %H:%M:%S
* %f 文件名 FilenameFormatItem -
* %l 行号 LineFormatItem -
* %T 制表符 TabFormatItem -
* %F 协程id FiberIdFormatItem -
* %N 线程名称 ThreadNameFormatItem -
* x 其他字符 StringFormatItem - 格式串m_pattern中其他字符,例如[]:,
*/
// 每个解析类xxxFormatItem都包含format方法,用于自定义格式化方法,例如MessageFormatItem就是将日志消息存放到输出流中
// 消息Format
class MessageFormatItem : public LogFormatter::FormatItem
{
public:
MessageFormatItem(const std::string &fmt){}
void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override
{os << event->getContent();}
};
到这里,我们应该能够明白该函数大体上的逻辑,自己按照这个逻辑顺一下代码,即可理解。
下面介绍几个我在学习过程中,疑问的点,可能方便你的理解。
问:函数中各变量的含义是什么?
答:
std::vector<std::tuple<std::string, std::string, int>> vec;
/*
定义一个vector,其中每个元素是一个三元组<std::string, std::string, int>
打住,刚刚前面不是说四元组吗?<符号,格式,解析类,type>
本函数中将“符号”和“解析类”的映射关系存到了map,在代码中可以看到
所以就简化为了三元组<符号,格式,type>
*/
std::string nstr;
// 存放格式化字符 m p r ...
语法函数知识点记录
std::stringstream
字符串流,需要引用头文件,像管理字符串一样,可以将输出先存到ss中,然后调用str()方法获取。
stringstream s;
s << "hello"
<< " "
<< "world"; // 将多个字符串放入字符串流中
cout << s.str() << endl;
s.str(""); // 清空字符串流
cout << s.str() << endl;
return 0;
std::function
std::function是一个函数包装器,该函数包装器模板能包装任何类型的可调用实体。
// std::function<void(int)> f; 表示function的对应f参数是int,返回值是void
#include <functional>
#include <iostream>
struct Foo
{
Foo(int num) : num_(num) {}
void print_add(int i) const { std::cout << num_ + i << '\n'; }
int num_;
};
void print_num(int i) { std::cout << i << '\n'; }
int main()
{
// 存储自由函数
std::function<void(int)> f_print_num = print_num;
f_print_num(5); // 5
// 存储lambda
std::function<void()> f_print_42 = []()
{
print_num(42);
};
f_print_42(); // 42
// 存储成员函数
std::function<void(const Foo &, int)> foo_print_add = &Foo::print_add;
const Foo foo(10);
foo_print_add(foo, 10); // 20
foo_print_add(foo, 20); // 30
return 0;
}
源码解析:
// map<字符串,FotmatItem的构造函数(需要传入参数str)>
// 这里使用的而是lambda表达式
std::function<FormatItem::ptr(const std::string &str) p=[](const std::string &fmt){
return FormatItem::ptr(new C(fmt));
}
static std::map<std::string, std::function<FormatItem::ptr(const std::string &str)>> s_format_items = {
#define XX(str, C) \
{ \
#str, [](const std::string &fmt) { return FormatItem::ptr(new C(fmt)); } \
}
XX(m, MessageFormatItem), // m:消息
XX(p, LevelFormatItem), // p:日志级别
XX(r, ElapseFormatItem), // r:累计毫秒数
XX(c, NameFormatItem), // c:日志名称
XX(t, ThreadIdFormatItem), // t:线程id
XX(n, NewLineFormatItem), // n:换行
XX(d, DateTimeFormatItem), // d:时间
XX(f, FilenameFormatItem), // f:文件名
XX(l, LineFormatItem), // l:行号
XX(T, TabFormatItem), // T:Tab
XX(F, FiberIdFormatItem), // F:协程id
XX(N, ThreadNameFormatItem), // N:线程名称
#undef XX
};
代码注释:
void LogFormatter::init()
{
// 解析核心代码
// m_pattern: %d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n
// 三元组<str, format, type>
// type==0 StringFormatItem 用于存放m_pattern的普通字符,如'[',']',':'
// type==1 其他的FormatItem
// str m p r ...
std::vector<std::tuple<std::string, std::string, int>> vec;
// 存放格式化字符 Y m d etc...
std::string nstr;
for (size_t i = 0; i < m_pattern.size(); ++i)
{
if (m_pattern[i] != '%')
{
// 如果不是%号
// nstr字符串后添加1个字符m_pattern[i]
nstr.append(1, m_pattern[i]);
continue;
}
if ((i + 1) < m_pattern.size())
{
// m_pattern[i]是% && m_pattern[i + 1] == '%' ==> 两个%,第二个%当作普通字符
if (m_pattern[i + 1] == '%')
{
nstr.append(1, '%');
continue;
}
}
// m_pattern[i]是% && m_pattern[i + 1] != '%', 需要进行解析
size_t n = i + 1; // 跳过'%',从'%'的下一个字符开始解析
int fmt_status = 0; // 是否解析大括号内的内容: 已经遇到'{',但是还没有遇到'}' 值为1
size_t fmt_begin = 0; // 大括号开始的位置
std::string str;
std::string fmt; // 存放'{}'中间截取的字符
// 从m_pattern[i+1]开始遍历
while (n < m_pattern.size())
{
// m_pattern[n]不是字母 & m_pattern[n]不是'{' & m_pattern[n]不是'}'
if (!fmt_status && (!isalpha(m_pattern[n]) && m_pattern[n] != '{' && m_pattern[n] != '}'))
{
str = m_pattern.substr(i + 1, n - i - 1);
break;
}
if (fmt_status == 0)
{
if (m_pattern[n] == '{')
{
// 遇到'{',将前面的字符截取
str = m_pattern.substr(i + 1, n - i - 1);
// std::cout << "*" << str << std::endl;
fmt_status = 1; // 标志进入'{'
fmt_begin = n; // 标志进入'{'的位置
++n;
continue;
}
}
else if (fmt_status == 1)
{
if (m_pattern[n] == '}')
{
// 遇到'}',将和'{'之间的字符截存入fmt
fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1);
// std::cout << "#" << fmt << std::endl;
fmt_status = 0;
++n;
// 找完一组大括号就退出循环
break;
}
}
++n;
// 判断是否遍历结束
if (n == m_pattern.size())
{
if (str.empty())
{
str = m_pattern.substr(i + 1);
}
}
}
if (fmt_status == 0)
{
if (!nstr.empty())
{
// 保存其他字符 '[' ']' ':'
vec.push_back(std::make_tuple(nstr, std::string(), 0));
nstr.clear();
}
// fmt:寻找到的格式
vec.push_back(std::make_tuple(str, fmt, 1));
// 调整i的位置继续向后遍历
i = n - 1;
}
else if (fmt_status == 1)
{
// 没有找到与'{'相对应的'}' 所以解析报错,格式错误
std::cout << "pattern parse error: " << m_pattern << " - " << m_pattern.substr(i) << std::endl;
m_error = true;
vec.push_back(std::make_tuple("<<pattern_error>>", fmt, 0));
}
}
if (!nstr.empty())
{
vec.push_back(std::make_tuple(nstr, "", 0));
}
/**
* s_format_items<格式标识:string,对应的对象:FormatItem>
* 不同的FormatItem可以实现自己的format方法进行日志格式化
* new 一个FormatItem对象,传入格式为fmt
*/
static std::map<std::string, std::function<FormatItem::ptr(const std::string &str)>> s_format_items = {
#define XX(str, C) \
{ \
#str, [](const std::string &fmt) { return FormatItem::ptr(new C(fmt)); } \
}
XX(m, MessageFormatItem), // m:消息
XX(p, LevelFormatItem), // p:日志级别
XX(r, ElapseFormatItem), // r:累计毫秒数
XX(c, NameFormatItem), // c:日志名称
XX(t, ThreadIdFormatItem), // t:线程id
XX(n, NewLineFormatItem), // n:换行
XX(d, DateTimeFormatItem), // d:时间
XX(f, FilenameFormatItem), // f:文件名
XX(l, LineFormatItem), // l:行号
XX(T, TabFormatItem), // T:Tab
XX(F, FiberIdFormatItem), // F:协程id
XX(N, ThreadNameFormatItem), // N:线程名称
#undef XX
};
for (auto &i : vec)
{
// 三元组<str, format, type>
if (std::get<2>(i) == 0)
{
// 存放需要解析的单项,这个为普通字符
m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(i))));
}
else
{
// 从s_format_items寻找,返回对应的对象
auto it = s_format_items.find(std::get<0>(i)); // 获得i的第1项
if (it == s_format_items.end())
{
// 格式未从s_format_items中找到
m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(i) + ">>")));
m_error = true;
}
else
{
// 将需要解析单项的xxxFormatItem存入
m_items.push_back(it->second(std::get<1>(i)));
}
}
// std::cout << "(" << std::get<0>(i) << ") - (" << std::get<1>(i) << ") - (" << std::get<2>(i) << ")" << std::endl;
}
// std::cout << m_items.size() << std::endl;
}