功能
Layout在log4cpp中是作为Appender的一个可选的策略接口而存在的。它的主要功能是将一个LoggingEvent的对象类型格式化为一个可以输出的字符串类型。
class Layout {
public:
// 作为基类的析构函数必须是virtual:如果你外面使用基类的指针来接收具体的子类地址,那么当你使用delete删除掉子基类的指针的时候,可以保证子类的虚方法也能够被调用
virtual ~Layout() { };
// 纯虚方法
// 功能就是将event格式化为string类型
virtual std::string format(const LoggingEvent& event) = 0;
};
从上面可以看到,它只是定义了一个format纯虚方法,功能就是将event格式化为一个string。
我们可以简单回忆一下LoggingEvent中的成员:
- 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, ¤tTime); //将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(), ¤tTime);
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 输出格式可以由用户指定,具体的格式