日志系统第三弹:日志消息和格式化模块的实现
- 一、日志消息模块的实现
- 二、日志格式化模块的设计
- 1.格式化占位符的约定
- 2.如何打印
- 1.各种零件类
- 1.抽象类
- 2.简单的零件类
- 3.日期零件类
- 4.非格式化数据零件类
- 2.Formatter
- 3.如何解析
- 三、日志格式化模块的实现
- 1.解析函数
- 2.createComponent
- 3.其他函数
- 四、测试
一、日志消息模块的实现
日志往往需要包含以下字段:
时间(年月日时分秒)
日志等级
文件名和行号
线程ID
日志器名称(日志器的唯一标识)
消息主体
日志等级主要分为:
DEBUG(调试信息)
INFO(一般信息)
WARNING(警告信息,表示可能出现问题,但尚未出错)
ERROR(错误信息,表示程序运行出现了问题,但通常不会阻止其继续运行)
FATAL(致命错误,表示程序无法继续运行)
当项目从调试阶段结束之后,准备发布了,一般都会设置一个日志等级,只允许等级>=限制等级的日志进行输出,这样就无需到处屏蔽对应的调试信息了
同样的,当项目出了BUG需要紧急调试修复时,就可以通过降低日志等级来恢复DEBUG级别的日志打印,无需到处取消对代码的屏蔽了
因此我们需要在提供一个最高级别的日志等级OFF,表示:当日志限制等级调整为OFF时,所有的日志都无法打印了
我们要对外提供一个用来将枚举值转为字符串的函数
struct LogLevel
{
enum class value
{
UNKNOWN = 0,
DEBUG,
INFO,
WARNING,
ERROR,
FATAL,
OFF
};
static std::string LogLevel_Name(value level)
{
switch (level)
{
case value::DEBUG:
return "DEBUG";
case value::INFO:
return "INFO";
case value::WARNING:
return "WARNING";
case value::ERROR:
return "ERROR";
case value::FATAL:
return "FATAL";
case value::OFF:
return "OFF";
}
return "UNKNOWN";
}
};
struct LogMessage
{
LogMessage(LogLevel::value level, const std::string &file, int line, const std::string &logger_name, const std::string &body)
: _level(level), _time(ns_helper::DateHelper::now()), _file(file), _line(line), _logger_name(logger_name), _body(body), _thread_id(std::this_thread::get_id()) {}
LogLevel::value _level;
time_t _time;
std::string _file;
int _line;
std::thread::id _thread_id;
std::string _logger_name;
std::string _body;
};
二、日志格式化模块的设计
日志格式化模块主要负责:
- 约定各种格式化占位符
- 允许用户传入自定义格式化字符串,在内部解析
- 把用户传入的消息主体格式化为日志消息
1.格式化占位符的约定
日志消息只包含这6个字段:
时间(年月日时分秒)
日志等级
文件名和行号
线程ID
日志器名称
消息主体
但是跟printf一样,用户可以在format格式化字符串当中打印纯字符printf("hello %s\n")
hello 就是用户打印的纯字符
用户还可以打印\n、\t等等格式字符
因此我们还要对纯字符,\n,\t进行约定:
%d 时间(date)
%r 日志等级(rate)
%f 文件名(file)
%l 行数(line)
%i 线程ID(id)
%b 消息主体(body)
%n 换行符(\n)
%t 水平制表符(\t)
%g 日志器名称(logger_name)
因为时间需要二次格式化(年月日,时分秒的格式化)
所以,我们直接借鉴日期具体的格式化占位符
"%Y-%m-%d %H:%M:%S"
2024-08-18 14:49:42
%Y : 年
%m : 月
%d : 天
%H : 时
%M : 分
%S : 秒
因此作为分割,我们规定{}是二级格式
2.如何打印
直接暴力解析并打印? 可以是可以,但是太不优雅了,而且代码的可扩展性不好,万一哪一天又想加一种格式化占位符呢
而且我们是同一个日志器打印出来的日志格式都是相同的,因此对于日志格式化模块来说,它只负责打印同一类型的日志
所以我们更希望它能够把日志格式给记录下来,到时候打印的时候直接根据记录的格式拼接好字符串即可
因此我们就需要把日志按照最小单位进行拆分
借助类似于建造者模式的思想,我们把组成日志的每个零件拆分一下,各成一类,都继承自同一个抽象类,重写同一个接口
1.各种零件类
1.抽象类
class FormatComponent
{
public:
using ptr=std::shared_ptr<FormatComponent>;
virtual ~FormatComponent(){}
// 将消息内容流入out流当中
virtual void format(std::ostream &out, const LogMessage &message) = 0;
};
因为流更好用,所以我们才不用字符串,而且字符串可以通过ostringstream的str函数拿到,且ostream是ostringstream的父类
2.简单的零件类
class LevelComponent : public FormatComponent
{
public:
virtual void format(std::ostream &out, const LogMessage &message)
{
out << LogLevel::LogLevel_Name(message._level);
}
};
class BodyComponent : public FormatComponent
{
public:
virtual void format(std::ostream &out, const LogMessage &message)
{
out<<message._body;
}
};
class FileComponent : public FormatComponent
{
public:
virtual void format(std::ostream &out, const LogMessage &message)
{
out<<message._file;
}
};
class LineComponent : public FormatComponent
{
public:
virtual void format(std::ostream &out, const LogMessage &message)
{
out<<message._line;
}
};
class NameComponent : public FormatComponent
{
public:
virtual void format(std::ostream &out, const LogMessage &message)
{
out<<message._logger_name;
}
};
class LineBreakComponent:public FormatComponent
{
public:
virtual void format(std::ostream& out,const LogMessage& message)
{
out<<"\n";
}
};
class TabComponent:public FormatComponent
{
public:
virtual void format(std::ostream& out,const LogMessage& message)
{
out<<"\t";
}
};
class ThreadIdComponent : public FormatComponent
{
public:
virtual void format(std::ostream &out, const LogMessage &message)
{
out << message._thread_id;
}
};
3.日期零件类
因为日期零件类有自己的二级格式,所以我们构造日期零件类的时候需要把二级格式传给他,由他进行保存
因为日期归他管
而解析日期格式有专门的系统调用函数strftime
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
根据format格式化字符串和tm日期结构体进行解析,将解析结果放到字符串s当中,字符串最大程度为max(防止缓冲区溢出的风险)
class DateComponent : public FormatComponent
{
public:
DateComponent(const std::string &format)
: _format(format) {}
virtual void format(std::ostream &out, const LogMessage &message)
{
const int max_size = _format.size() + 32;
char buf[max_size] = {0};
time_t now = message._time;
struct tm *tm = localtime(&now);
strftime(buf, max_size - 1, _format.c_str(), tm);
out << buf;
}
private:
std::string _format;
};
4.非格式化数据零件类
对于非格式化数据的打印,直接打印原本的字符即可,因此非格式化数据零件类无需LogMessage而是需要一个string,所以他在构造的时候,也是需要给一个string的
class OtherComponent : public FormatComponent
{
public:
OtherComponent(const std::string &body)
: _body(body) {}
virtual void format(std::ostream &out, const LogMessage &message)
{
out << _body;
}
private:
std::string _body;
};
2.Formatter
要求构造时就要传入pattern日志模式,然后我们进行解析,将对应的零件类保存在vector当中
然后提供两个消息的格式化接口,一个是可以直接打印日志,另一个是返回string类型的日志
默认格式是这样的:
"[%d{%Y-%m-%d %H:%M:%S}] [%r] [%f:%l] [%g] [%i] %b%n"
日期[年月日 时分秒] [日志等级] [文件名:行号] [日志器名称] [线程ID] 消息主体 换行
class Formatter
{
public:
// 日期[年月日 时分秒] [日志等级] [文件名:行号] [日志器名称] [线程ID] 消息主体 换行
Formatter(const std::string &pattern="[%d{%Y-%m-%d %H:%M:%S}] [%r] [%f:%l] [%g] [%i] %b%n") {}
void format(std::ostream &out, const LogMessage &message) {}
std::string format(const LogMessage &message) {}
private:
bool parse() {}
FormatComponent::ptr createComponent(const std::string& symbol,const std::string& val){}
// 格式
std::string _pattern;
std::vector<FormatComponent::ptr> _components;
};
注意: %%
是%
的转义,也就是打印%自己。相当于\\
就是\
3.如何解析
维护一个parse_ret用来存放解析结果,里面的类型是pair<int,int>,first是d、r、f、l、i、b、n、t、g、空串
,second是OtherComponent 要的body或者DateComponent 要的format
如果first为空,则代表该解析结果是OtherComponent
具体步骤:
遍历_pattern,维护一个非结构化字符串unformat_val ,如果当前字符不是%,则放入unformat_val的尾部,往后走一步
否则,分为三种情况:
(1)下一个字符是%,则将%放入unformat_val的尾部
(2)下一个字符是d,则往后找} , 将{}里面的格式提出来,存为val, 后续要传递给createComponent作为参数
(3)否则,就按普通的格式化字符来匹配
一旦遇到格式化字符,最需要先把unformat_val字符串先抛入vec当中保存下来
大家画个图,一下子就出来
三、日志格式化模块的实现
1.解析函数
其实就是个分类讨论而已,顺着思路写很简单
bool parse()
{
// 1. 解析字符串
std::string unformat_val;
size_t index = 0;
size_t start = 0;
std::vector<std::pair<std::string, std::string>> parse_ret;
while (index < _pattern.size())
{
if (_pattern[index] != '%')
{
unformat_val += _pattern[index++];
}
else if (index + 1 >= _pattern.size())
{
std::cout << "解析失败,因为%后面没有字符,pattern:" << _pattern << "\n";
return false;
}
else if (_pattern[index + 1] == '%')
{
unformat_val += '%';
index += 2;
}
else if (_pattern[index + 1] == 'd')
{
if (!unformat_val.empty())
{
// 首先先将unformat_val放进去
parse_ret.push_back({"", unformat_val});
unformat_val.clear();
}
// 取出{}之内的二级格式
size_t start = _pattern.find('{', index + 2), end = _pattern.find('}', index + 2);
if (start == std::string::npos || end == std::string::npos)
{
std::cout << "解析失败,因为%d后面未找到{},pattern:" << _pattern << "\n";
return false;
}
parse_ret.push_back({"d", _pattern.substr(start + 1, (end - 1) - (start + 1) + 1)});
index = end + 1;
}
else
{
if (!unformat_val.empty())
{
// 首先先将unformat_val放进去
parse_ret.push_back({"", unformat_val});
unformat_val.clear();
}
parse_ret.push_back({_pattern.substr(index + 1, 1), ""});
index += 2;
}
}
// 注意:非格式化字符是可以充底的,所以这里需要再次搞一下
if (!unformat_val.empty())
{
// 首先先将unformat_val放进去
parse_ret.push_back({"", unformat_val});
unformat_val.clear();
}
// 2. 构造组成成分
for (auto &elem : parse_ret)
{
_components.push_back(createComponent(elem.first, elem.second));
}
return true;
}
2.createComponent
FormatComponent::ptr createComponent(const std::string &symbol, const std::string &val)
{
if (symbol == "d")
return std::make_shared<DateComponent>(val);
else if (symbol == "r")
return std::make_shared<LevelComponent>();
else if (symbol == "f")
return std::make_shared<FileComponent>();
else if (symbol == "l")
return std::make_shared<LineComponent>();
else if (symbol == "g")
return std::make_shared<NameComponent>();
else if (symbol == "i")
return std::make_shared<ThreadIdComponent>();
else if (symbol == "b")
return std::make_shared<BodyComponent>();
else if (symbol == "n")
return std::make_shared<LineBreakComponent>();
else if (symbol == "t")
return std::make_shared<TabComponent>();
else if (symbol == "")
return std::make_shared<OtherComponent>(val);
else
{
std::cout << "不存在该格式化字符: %" << symbol << "\n";
abort();
return FormatComponent::ptr(); // 返回一个空指针
}
}
3.其他函数
// 日期[年月日 时分秒] [日志等级] [文件名:行号] [日志器名称] [线程ID] 消息主体 换行
Formatter(const std::string &pattern = "[%d{%Y-%m-%d %H:%M:%S}] [%r] [%f:%l] [%g] [%i] %b%n")
: _pattern(pattern)
{
if (!parse())
{
std::cout << "解析pattern失败!, pattern: " << _pattern << "\n";
abort();
}
}
void format(std::ostream &out, const LogMessage &message)
{
for (auto &elem : _components)
{
elem->format(out, message);
}
}
std::string format(const LogMessage &message)
{
std::ostringstream oss;
format(oss, message);
return oss.str();
}
四、测试
g++ -o mytest test.cc -std=c++11 -pthread
一定要链接pthread库,否则无法拿到正确的thread_id
int main()
{
LogMessage message(LogLevel::value::WARNING, __FILE__, __LINE__, "default_logger", "I am here, World");
std::shared_ptr<Formatter> formatter = std::make_shared<Formatter>();
std::cout << formatter->format(message);
formatter = std::make_shared<Formatter>("abc%%%%def%t%d{%Y %m %d}%%%r %b%n");
std::cout << formatter->format(message);
return 0;
}
wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/test$ ./mytest
[2024-08-18 17:50:18] [WARNING] [test.cc:6] [default_logger] [140422774794048] I am here, World
abc%%def 2024 08 18%WARNING I am here, World
以上就是日志系统第三弹:日志消息和格式化模块的实现的全部内容