log4cpp源码阅读:Layout组件解析
发布日期:2022-03-16 03:25:42 浏览次数:27 分类:技术文章

本文共 20731 字,大约阅读时间需要 69 分钟。

功能

Layout在log4cpp中是作为Appender的一个可选的策略接口而存在的。它的主要功能是将一个LoggingEvent的对象类型格式化为一个可以输出的字符串类型。

class Layout {
public: // 作为基类的析构函数必须是virtual:如果你外面使用基类的指针来接收具体的子类地址,那么当你使用delete删除掉子基类的指针的时候,可以保证子类的虚方法也能够被调用 virtual ~Layout() {
}; // 纯虚方法 // 功能就是将event格式化为string类型 virtual std::string format(const LoggingEvent& event) = 0; };

从上面可以看到,它只是定义了一个format纯虚方法,功能就是将event格式化为一个string。

我们可以简单回忆一下中的成员:

  • categoryName : category的名称
  • priority : 日志操作的等级
  • ndc : 是使用NDC::get()获取的栈顶的fullMessage
  • message : 要进行日志操作的消息
  • threadName : 发出日志请求所在的线程名称,内部是使用threading::getThreadID来获取的
  • timeStamp : 发出日志请求时候的时间

我们来看一下Layout的具体几个实现类

在这里插入图片描述

SimpleLayout类

class SimpleLayout : public Layout {
public: SimpleLayout() {
} ; virtual ~SimpleLayout() {
}; virtual std::string format(const LoggingEvent& event);}; std::string SimpleLayout::format(const LoggingEvent& event) {
std::ostringstream message; // 利用Priority来讲Priority::Value转换为可读的字符串 const std::string& priorityName = Priority::getPriorityName(event.priority); //首先流每次输出宽度,并且是靠左,也就是说右侧可能有空格 Priority::MESSAGE_SIZE=8 message.width(Priority::MESSAGE_SIZE);message.setf(std::ios::left); //输出格式:首先是操作级别名称 : 具体的日志消息 message << priorityName << ": " << event.message << std::endl; return message.str(); }

SimpleLayout的输出格式是: priorityName[当priorityName长度不够Priority::MESSAGE_SIZE的时候,这儿会有留空]: message 换行

BasicLayout类

class BasicLayout : public Layout {
public: BasicLayout(); virtual ~BasicLayout(); virtual std::string format(const LoggingEvent& event);}; std::string BasicLayout::format(const LoggingEvent& event) {
std::ostringstream message; const std::string& priorityName = Priority::getPriorityName(event.priority); message << event.timeStamp.getSeconds() << " " << priorityName << " " << event.categoryName << " " << event.ndc << ": " << event.message << std::endl; return message.str(); }

BaseicLayout类的输出格式是: 日志发出时的时刻(秒数描述表示) 日志等级名称 category名称 ndc内容 : 日志内容 换行

PatternLayout类

PatternLayout可以使用指定格式来格式化LoggingEvent对象为字符串。类似printf, vsprintf,比如"%d"输出日志的发出具体时间,"%p"表示输出日志的等级名称, m表示输出日志的消息。。。。

我们可以通过setConversionPattern方法来设置指定的格式化字符串,我们来看一下这个方法

PatternLayout::setConversionPattern

class PatternLayout : public Layout {
private: std::string _conversionPattern; // 用来设置传过来的格式化字符串 public: class PatternComponent {
//一个格式化组件 public: inline virtual ~PatternComponent() {
}; // 将event中的某些信息输出到out中 virtual void append(std::ostringstream& out, const LoggingEvent& event) = 0; }; virtual void setConversionPattern(const std::string& conversionPattern);}void PatternLayout::clearConversionPattern() {
for(ComponentVector::const_iterator i = _components.begin(); i != _components.end(); ++i) {
delete (*i); } _components.clear(); _conversionPattern = "";}// 设置格式化字符串void PatternLayout::setConversionPattern(const std::string& conversionPattern) {
std::istringstream conversionStream(conversionPattern); //一个字符串输入流 std::string literal; // 用来存储某些纯字符串值 char ch; PatternLayout::PatternComponent* component = NULL; int minWidth = 0; size_t maxWidth = 0; clearConversionPattern(); // 一个一个遍历字符,结果保存在ch中 while (conversionStream.get(ch)) {
// 当前流字符是'%',则说明下一个字符应该是应该是具体的格式化字符串 // 那么内部肯定要解析具体的格式化字符串 if (ch == '%') {
// 这儿主要是处理,形如%10.20m的情况 // readPrefix; {
char ch2; conversionStream.get(ch2); // 如果是数字,则读取下一个数字 if ((ch2 == '-') || ((ch2 >= '0') && (ch2 <= '9'))) {
conversionStream.putback(ch2); //ch2只是用来探测,当发现符合,把它放回去 conversionStream >> minWidth; conversionStream.get(ch2); //ch2读取数字下一个字符 } // 如果是小数点,那么他会退到这儿的具体格式是形如%10.20的格式 if (ch2 == '.') {
conversionStream >> maxWidth; // 读取小数点后面的字符 } else {
conversionStream.putback(ch2); // 如果不是的话,还把ch2放回去,不影响下面的解析操作 } } // 这儿的字符肯定是具体的格式化字符,这儿也对错误进行了处理 // 就是如果conversionPattern的最后一个字符是%的时候,到这儿肯定是读取失败的,然后就会抛出ConfigureFailure异常 if (!conversionStream.get(ch)) {
std::ostringstream msg; msg << "unterminated conversion specifier in '" << conversionPattern << "' at index " << conversionStream.tellg(); throw ConfigureFailure(msg.str()); } // 下面处理的是形如%m{%Y-%m-%d %H:%M:%S}的情况 std::string specPostfix = ""; // read postfix {
char ch2; if (conversionStream.get(ch2)) {
//ch2是格式化字符后面的字符,比如%m{..},那么这儿ch2就是{ // 如果格式化字符后面有大括号,那么大括号里面的内容会保存到specPostfix if (ch2 == '{') {
while(conversionStream.get(ch2) && (ch2 != '}')) specPostfix += ch2; } else {
conversionStream.putback(ch2); // 否则取消之前的操作,不影响下次正常读取 } } } // 到目前为止ch肯定是一个特别的格式化字符了,我们可以根据下面的操作,明白各种格式化字符的具体含义 switch (ch) {
case '%': // 如果出现两次%,他会PatternLayout类,会把它当做一个% literal += ch; break; case 'm': // 如果出现%m, 那么表示要输出的是消息,至于XXXComponent会在后面进行介绍 // 现在只要知道MessageComponent是PatternComponent的基类就可以了 component = new MessageComponent(); break; case 'n': // 如果出现%n,那么表示要输出的内容是一个换行符 {
std::ostringstream endline; endline << std::endl; literal += endline.str(); } break; case 'c': // 如果出现%c,则说明要输出的内容是category的名称 component = new CategoryNameComponent(specPostfix); break; // 如果出现%d,则说明要输出的内容是日志产生的时间,这儿我们需要注意的是,他居然把 // specPostfix,就是大括号里面的内容传给下面类的构造函数,原因,这儿可以提前说一个 // 比如%d 他只会输出默认的日志格式,但是如果你传入%d{%H:%M:%S,%l},他会只输出时间 // 不会输出日期,具体可以查看TimeStampComponent的实现,我们下面会有介绍的,总之 // {}中的内容可以是strftime支持的格式,另外在加上%l表示输出毫秒 case 'd': component = new TimeStampComponent(specPostfix); break; case 'p': // 如果出现%p,则说明要输出的内容是日志等级的字符串形式 component = new PriorityComponent(); break; case 'r': // 如果输出的是%r,则说明要输出的内容是 日志产生时刻到进程启动时刻的时间间隔 component = new MillisSinceEpochComponent(); break; case 'R': // 如果输出的是%R,则说明要输出的内容是 日志产生的时候的时间(但是是秒数) component = new SecondsSinceEpochComponent(); break; case 't': // 如果输出的是%t, 则说明要输出的内容是线程名称 component = new ThreadNameComponent(); break; case 'u': // 如果输出的是%u, 则说明要输出的内容是处理器运行到这儿(或者说执行到这儿)的时间 component = new ProcessorTimeComponent(); break; case 'x': // 如果输出的是%x, 则说明要输出的内容是category.ndc的内容 component = new NDCComponent(); break; default: //这儿是抛出异常 std::ostringstream msg; msg << "unknown conversion specifier '" << ch << "' in '" << conversionPattern << "' at index " << conversionStream.tellg(); throw ConfigureFailure(msg.str()); } // 表示当前解析的字符单元是格式化字符串 if (component) {
if (!literal.empty()) {
_components.push_back(new StringLiteralComponent(literal)); literal = ""; } // 如果处理到形如%10.20m的时候,那面下面会被执行 // 此时minWidth=10 maxWidth=20 if ((minWidth != 0) || (maxWidth != 0)) {
component = new FormatModifierComponent(component, std::abs(minWidth), maxWidth, minWidth < 0); // 这个类的内部使用的是装饰着模式 minWidth = maxWidth = 0; } _components.push_back(component); // 会将构造好的PatternComponent对象放到_components中 component = NULL; } } else {
literal += ch; //如果ch不是格式化字符,则将这个字符添加到literal中,正如前面介绍的,他是用来存储普通字符串 } } // 当上面的循环执行完毕,那么所有的conversionPattern中的内容都会被解析完毕,我们可以保证的 // 是前面所有格式化字符串的内容都会被构建成PatternComponent对象的形式,并且会被保存到_components中 // 但是输出普通文本的时机是当解析到格式化字符串的时候,才会将前次产生的字符串literal进行输出,但是如果 // conversionPattern的最后一部分内容不是普通文本的时候,这个时候,字符串内容仍然保存在literal中 // 所以当literal不为空的时候,我们还需要将普通文本也进行输出 if (!literal.empty()) {
_components.push_back(new StringLiteralComponent(literal)); } _conversionPattern = conversionPattern; //最后将设置过来的字符串保存到_conversionPattern属性中}

从上面可以看出,设置格式化字符串的时机是在setConversionPattern方法中,解析格式化字符串的时机也是在setConversionPattern方法中,内部会将解析到的内容以PatternComponent具体子类的对象来表示,并且最终完整结果保存到_components容器中

PattrenLayout支持的格式化字符串表

有如下映射关系:

在这里插入图片描述

PatternLayout::format

std::string PatternLayout::format(const LoggingEvent& event) {
std::ostringstream message; // 创建一个输出字符串流来保存最终结果 // 挨个调用PatternComponent的append方法来讲内容输出到message中 for(ComponentVector::const_iterator i = _components.begin(); i != _components.end(); ++i) {
(*i)->append(message, event); } return message.str(); // 返回message的内容 }

下面我们来详细探索一下PatternComponent相关组件的功能。

PatternComponent

class PatternLayout {
private: ...public: // 一个表达式组件 class PatternComponent {
public: inline virtual ~PatternComponent() {
}; // 将event中的某些信息输出到out中 // out是输出参数 virtual void append(std::ostringstream& out, const LoggingEvent& event) = 0; }; virtual void setConversionPattern(const std::string &conversionPattern)throw(ConfigureFailure);};

其实PatternComponent就是一个接口,它的功能就是允许将event中的某些信息输出到out中

下面来看看它的具体子类实现

StringLiteralComponent

// 原样输出通过构造器传进来的字符串struct StringLiteralComponent : public PatternLayout::PatternComponent {
// 构造器,设置_literal的值 StringLiteralComponent(const std::string& literal) : _literal(literal) {
} // 实现基类PatternComponent 的方法 virtual void append(std::ostringstream& out, const LoggingEvent& event) {
out << _literal; // 将从构造器中传进来的内容输出到out中 } private: std::string _literal; // 保存要进行输出的字面内容 };

以及:

struct MessageComponent : public PatternLayout::PatternComponent {
virtual void append(std::ostringstream& out, const LoggingEvent& event) {
out << event.message; // 原样输出event中的message属性 } }; struct NDCComponent : public PatternLayout::PatternComponent {
virtual void append(std::ostringstream& out, const LoggingEvent& event) {
out << event.ndc; // 原样输出event中的ndc属性 } }; struct PriorityComponent : public PatternLayout::PatternComponent {
virtual void append(std::ostringstream& out, const LoggingEvent& event) {
out << Priority::getPriorityName(event.priority); // 输出event的对应的权限等级 } }; struct ThreadNameComponent : public PatternLayout::PatternComponent {
virtual void append(std::ostringstream& out, const LoggingEvent& event) {
out << event.threadName; // 原样输出event中的threadName } }; struct ProcessorTimeComponent : public PatternLayout::PatternComponent {
virtual void append(std::ostringstream& out, const LoggingEvent& event) {
out << std::clock(); // 输出当前时间(其实是处理器运行到这儿的时间) } };struct SecondsSinceEpochComponent : public PatternLayout::PatternComponent {
virtual void append(std::ostringstream& out, const LoggingEvent& event) {
out << event.timeStamp.getSeconds(); // 输出的是当前日志产生的时间(单位是秒) } }; struct MillisSinceEpochComponent : public PatternLayout::PatternComponent {
// 输出的内容日志记录产生时间到进程启动时间的间隔(单位是毫秒) virtual void append(std::ostringstream& out, const LoggingEvent& event) {
int64_t t = event.timeStamp.getSeconds() - TimeStamp::getStartTime().getSeconds(); //获取的是日志产生时间到进程启动时间的间隔 t *= 1000; //将秒转换为毫秒 t += event.timeStamp.getMilliSeconds() - TimeStamp::getStartTime().getMilliSeconds(); out << t; } };

下面会介绍几个复杂点的PatternComponent

CategoryNameComponent

struct CategoryNameComponent : public PatternLayout::PatternComponent {
//specifier是一个可以转换为int类型的字符串或者空字符串 CategoryNameComponent(std::string specifier) {
if (specifier == "") {
_precision = -1; } else {
std::istringstream s(specifier); // 利用字符串流将性如"123","222"字符串转换为int类型的数字,相当于atoi喊函数 s >> _precision; } } // 实现PatternComponent具体的追加方法 virtual void append(std::ostringstream& out, const LoggingEvent& event) {
if (_precision == -1) {
out << event.categoryName; // 当_precision == -1则直接将category名称进行原样输出 } else {
std::string::size_type begin = std::string::npos; // 这儿的逻辑就是倒数第_precision个.后面的字符 // 比如如果categoryName = "pparent.parent.son" // 那么%c{1}返回的是son for(int i = 0; i < _precision; i++) {
begin = event.categoryName.rfind('.', begin - 2); if (begin == std::string::npos) {
begin = 0; break; } begin++; // 这儿加1是为了下面那句代码考虑的,保证退出循环时候,begin位于.的后一个字符 } if (begin == std::string::npos) {
begin = 0; } out << event.categoryName.substr(begin);//截取地倒数_precision个.后面的字符 } } private: int _precision; };

从上面可以分析出,CategoryNameComponent除了可以输出category的名称,还可以指定一个presion,来指定到底输出几个名称。比如categoryName为"parent.parent.son",那么如果格式化字符串是"%c{1}“则输出内容为"son”, 如果格式化字符串是"%c{2}“则输出内容是"parent.son”…

TimeStampComponent

struct TimeStampComponent : public PatternLayout::PatternComponent {
static const char* const FORMAT_ISO8601; // 几个字符串常量,每个常量都是具体的格式化字符串 static const char* const FORMAT_ABSOLUTE; static const char* const FORMAT_DATE; //从下面的源码中,我们可以发现,timeFormat除了是形如:%H:%M:%S的格式 //还可以是一些格式的名称,比如ISO8601,则他的内部使用格式就是:"%Y-%m-%d %H:%M:%S,%l", //当空的时候,也是使用这个格式,所以当我们指定%d或者%d{} 他能够输出完整的时间 //比如ABSOLUTE,则他的格式就是:"%H:%M:%S,%l",只包含时间,没有日期信息 //比如DATE,则他内部使用的格式是:"%d %b %Y %H:%M:%S,%l" TimeStampComponent(std::string timeFormat) {
// 处理timeFormat使用某种固定格式的名称 if ((timeFormat == "") || (timeFormat == "ISO8601")) {
timeFormat = FORMAT_ISO8601; } else if (timeFormat == "ABSOLUTE") {
timeFormat = FORMAT_ABSOLUTE; } else if (timeFormat == "DATE") {
timeFormat = FORMAT_DATE; } // 根据timeFormat中是否含有%l来确定是否要显示毫秒部分 std::string::size_type pos = timeFormat.find("%l"); if (pos == std::string::npos) {
_printMillis = false; _timeFormat1 = timeFormat; } else {
// 有的话 _printMillis = true; //假如说,timeFormat为%H:%M:%S,%lms,则 _timeFormat1 = timeFormat.substr(0, pos); //截取非毫秒部分, 为%H:%M:%S, _timeFormat2 = timeFormat.substr(pos + 2);//截取毫秒部分 为ms } } // 实现PatternComponent中的append方法 virtual void append(std::ostringstream& out, const LoggingEvent& event) {
struct std::tm currentTime; std::time_t t = event.timeStamp.getSeconds();//获取的是utc时间(就是距离1970-1-1 00:00:00的时间间隔),单位是秒数 localtime(&t, &currentTime); //将utc时间转换为本地时间 char formatted[100]; std::string timeFormat; if (_printMillis) {
//如果显示毫秒部分,因为下面strftime不支持格式化毫秒部分,所以首先把格式化毫秒部分的内容给处理出来 std::ostringstream formatStream; formatStream << _timeFormat1 << std::setw(3) << std::setfill('0') << event.timeStamp.getMilliSeconds() << _timeFormat2; timeFormat = formatStream.str(); } else {
timeFormat = _timeFormat1; } std::strftime(formatted, sizeof(formatted), timeFormat.c_str(), &currentTime); out << formatted; } private: std::string _timeFormat1; // 非毫秒部分的格式 std::string _timeFormat2; // 毫秒部分的格式 bool _printMillis; // 用来标志是否输出毫秒部分的内容};const char* const TimeStampComponent::FORMAT_ISO8601 = "%Y-%m-%d %H:%M:%S,%l";const char* const TimeStampComponent::FORMAT_ABSOLUTE = "%H:%M:%S,%l";const char* const TimeStampComponent::FORMAT_DATE = "%d %b %Y %H:%M:%S,%l";

这个方法内部实现使用的是strftime,所以他内部的设置的日期具体字符串是和strftime中的字符串是完全一致的。并且一般平时,我们只需要 使用d就能够输出我们需要的格式了,因为他默认输出格式是%Y-%m-%d %H:%M:%S,%l,包含日期 时间,和毫秒。有一点需要说明,向输出毫秒使用%l,可能 这个格式strftime可能不支持。

FormatModifierComponent

struct FormatModifierComponent : public PatternLayout::PatternComponent {
// component 是要进行包装的组件对象 // minWidth 用来设置的最小宽度 // maxWidth 用来进行设置的最大宽度 // alignLeft 是到输出的内容长度小于指定的minWidth用来指定文字的对齐方法,如果是左边则右不空,否则左不空 FormatModifierComponent(PatternLayout::PatternComponent* component, size_t minWidth, size_t maxWidth, bool alignLeft) : _component(component) , _minWidth(minWidth), _maxWidth(maxWidth), _alignLeft(alignLeft) {
} virtual ~FormatModifierComponent() {
delete _component; } // 实现基类的append方法 virtual void append(std::ostringstream& out, const LoggingEvent& event) {
std::ostringstream s; _component->append(s, event); // 装饰着模式 std::string msg = s.str(); // 如果处理最大宽度,删除掉最大长度的后面的内容 if (_maxWidth > 0 && _maxWidth < msg.length()) {
msg.erase(_maxWidth); } // 需要填充字符,当这个值大于0则需要填充 size_t fillCount = _minWidth - msg.length(); if (_minWidth > msg.length()) {
if (_alignLeft) {
out << msg << std::string(fillCount, ' '); } else {
out << std::string(fillCount, ' ') << msg; } } else {
out << msg; //要输出内容的长度大于等于最小宽度,则原样输出 } } private: PatternLayout::PatternComponent* _component; size_t _minWidth; size_t _maxWidth; bool _alignLeft; }; const char* PatternLayout::DEFAULT_CONVERSION_PATTERN = "%m%n"; const char* PatternLayout::SIMPLE_CONVERSION_PATTERN = "%p - %m%n"; const char* PatternLayout::BASIC_CONVERSION_PATTERN = "%R %p %c %x: %m%n"; const char* PatternLayout::TTCC_CONVERSION_PATTERN = "%r [%t] %p %c %x - %m%n";

这个类是一个典型的装饰者模式,可以使用这个类来控制已有的PatternLayout的输出宽度

PatternLayout总结

回过头来看一下 PatternLayout::setConversionPattern的实现:

  • 它提取conversionPattern中的每一个字符,当提取的字符是以%开头,它会尝试提取形如%10.20m这样的格式化字符串,将10保存到minWidth 将20保存到maxWidth中,然后提取格式化字符,然后会尝试提取大括号中的内容将内容保存到specPosfix中,最后会根据提取的格式化字符来 创建具体的PatternComponent子类。
  • 会将创建的子类指针保存到component中,对于非%开头的内容,他会将字符保存到literal中,循环体的最后 会根据compoent是否为空,来决定是否输出上次literal文本中的内容,使用的是_components.push_back(new StringLiteralComponent(literal)); 然后输出本次component中的内容,然后程序根据maxWidth和minWidth的状态来决定是否使用FormatModifierComponent来设置component输出的 最小宽度和最大宽度,按照上述步骤,循环执行,直到所有的格式化字符串都转换为PatternComponent对象添加_components中。
  • 在循环体外,他会根据 literal的状态来确定是否conversionPattern的最后内容是纯字符串,如果是则将最后的字符串内容也转换为StringLiteralComponent对象添加到_components中。

PatternLayout::format方法实现:

  • 首先新建一个字符串输出流来获取最终的结果,
  • 然后挨个遍历_components中的PatternComponent对象,调用每个对象的append方法,将上一步创建的字符串输出流作为append方法实际上参数来接收结果
  • 最后遍历完毕后,那么最终的结果就会保存到是输出流中,就可以得到想要的结果

总结

Layout的功能就是将一个LoggingEvent对象格式化为一个可读的字符串。

对于SimpleLayout, BasicLayout, PatternLayout三个具体具体的Layout的实现类

  • SimpleLayout 格式是: 权限 : 消息 换行
  • BasicLayout 格式是: 时刻(秒数为单位) 日志等级 category名称 ndc内容: 消息 换行
  • PatternLayout 输出格式可以由用户指定,具体的格式

转载地址:https://blog.csdn.net/zhizhengguan/article/details/123226102 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:log4cpp源码阅读:Category组件学习
下一篇:log4cpp源码阅读:TimeStamp工具类

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2024年04月07日 13时47分54秒