豚鼠系列血肉之花在线:glog简单分析
来源:百度文库 编辑:九乡新闻网 时间:2024/04/24 13:05:49
glog简单分析
项目组一直使用google的glog开源库进行日志输出, 花时间研究了一下, 做些分享.
这里就不分析它的使用方式了, 还是比较简单的, 几乎可以不用配置就直接使用了.另外, 如果真的需要配置的话, glog和一般的日志系统(如log4系列)是不太一样的, 后者一般使用配置文件, 而glog是在命令行参数中指定的.对比优缺点, 配置文件做的配置可能更加强大一些, 不过命令行配置虽然简单但是也不失灵活.具体使用方式还是自己去看看吧:)之所以这里会写这篇文章是因为看到一些同学说glog用了大量的宏技巧, 看得晕, 其实仔细看并不复杂,另外,它的实现也比较精巧,用过了那些很”重”的log库之外,不失为另一个好的参考.
1) 一般的日志输出流程
以一个日志输出的完整流程来做说明吧.
glog一般使用VLOG(num)或者LOG(severity)两种形式的宏进行输出.就以LOG(severity)为例进行说明, VLOG(num)系列应该类似了.
比如要输出一条日志信息, 如LOG(ERROR) << "hello world"
LOG宏的定义是:
1
#define LOG(severity) COMPACT_GOOGLE_LOG_ ## severity.stream()
可以看到它将根据具体的名字展开为另一个宏,因为上面选择的输出级别是ERROR, 所以展开的名称为COMPACT_GOOGLE_LOG_ERROR
继续跟进这个宏的定义:
1
#define COMPACT_GOOGLE_LOG_ERROR
@ac_google_namespace
@::LogMessage( \
2
__FILE__, __LINE__,
@ac_google_namespace
@::ERROR)
可以看到这个宏的作用其实就是创建了一个名为LogMessage的类, 构造函数的输入参数包括了:文件名, 行号, 日志级别
继续跟进LogMessage的构造函数:
1
LogMessage::LogMessage(
const
char
* file,
int
line, LogSeverity severity) {
2
Init(file, line, severity, &LogMessage::SendToLog);
3
}
这一次, 多了一个参数, 名为SendToLog的类成员函数指针.这里需要说明的是, 其实这个函数指针参数的目的是进行日志输出的操作,但是是可以配置的,因为LogMessage还有另外重载的构造函数其中有这个参数的,只是这里说明的是最简单的情况,所以就考虑了这个函数指针默认为SendToLog的情况,从名字可以猜测到,该函数的作用是向磁盘进行日志输出操作,另外glog中还可以向标准输出进行输出,如果有必要,应该还可以通过网络对某个地址端口进行输出–这些情况我都没有进一步跟进了,只想说明该函数指针的作用是向不同的介质输出日志,而使用函数指针作为参数就是为了让这个行为可以配置.
继续往下走,看看LogMessage::Init函数做的事情.这里不贴代码了,有兴趣的可以自己跟进看看.简单来说做的事情是:初始化日志输入的流缓冲区,初始化该日志的时间,格式,找到日志打印的文件名等.
好了,至此,一个完整的LogMessage就创建完毕.可以看到,在glog中,任何的一条日志信息,最终都会对应到一个新创建的LogMessage对象,有什么具体的好处呢,后面会分析到.
上面的日志输出流程,其实还没有完,因为还要进行流输入操作呢,就是输入”hello world”字符串.没错,退回头看看LOG宏的定义
1
#define LOG(severity) COMPACT_GOOGLE_LOG_ ## severity.stream()
如果说, 前面的COMPACT_GOOGLE_LOG_ ## severity创建了一个LogMessage类对象,那么其实这个宏最终的结果是返回这个LogMessage类对象的成员stream(), 在提到LogMessage::Init函数的时候就提到过,该函数初始化了流输入缓冲区.
来看LogMessage类中该流输入类的定义:
01
class
GOOGLE_GLOG_DLL_DECL LogStream :
public
std::ostrstream {
02
#ifdef _MSC_VER
03
# pragma warning(
default
:
4275
)
04
#endif
05
public
:
06
LogStream(
char
*buf,
int
len,
int
ctr)
07
: ostrstream(buf, len),
08
ctr_(ctr) {
09
self_ =
this
;
10
}
11
int
ctr()
const
{
return
ctr_; }
12
void
set_ctr(
int
ctr) { ctr_ = ctr; }
13
LogStream* self()
const
{
return
self_; }
14
private
:
15
int
ctr_;
// Counter hack (for the LOG_EVERY_X() macro)
16
LogStream *self_;
// Consistency check hack
17
};
其实很简单, 从std::ostrstream中继承过来,构造函数中有一个缓冲区就好了.在LogMessage::Init类中,定义该缓冲区的大小为:
1
buf_ =
new
char
[kMaxLogMessageLen+
1
];
其中
1
const
size_t LogMessage::kMaxLogMessageLen =
30000
;
这里可以看到glog限制的一条日志长度大小为30000 byte.
其实我不明白为什么这样做, 如果每次打印一条日志都要分配一个这么大的缓冲区,而其实很多时候都是用不了这么大的,岂不是浪费了,难道说一次过分配足够大的内存可以有效率的提高,回头要尝试修改一下这个做法,不从std::ostrstream中继承自己管理缓冲区了,而是直接使用标准的std::ostringstream看看效率有没有大的变化.
好了,到此为止,流输入也有了,尽管往里面写数据就好.那么什么时候进行输出呢?我在今年早些时候,也曾经因为这个问题批判过C++, 由于对流输入操作结束位置判断方式的缺失,glog采用的是在这个创建的临时LogMessage对象(说它是”临时对象”是因为它是匿名的,因此不会保存,创建了就会被释放)被释放的时候,在析构函数中做输出操作:
1
LogMessage::~LogMessage() {
2
Flush();
3
delete allocated_;
4
}
Flush函数不详细分析了,主要做的事情就是将日志和之前得到的日期等进行格式化,最后调用注册的输出日志用的函数指针进行输出操作.
到此为止,一条glog日志就输出完成了.
再回头看看,实际上这里应该还有一些东西是需要全局使用的,比如有多条日志同时向一个文件进行输出的时候,需要对文件进行加锁,还比如要更新一些统计的数据,如每个级别的日志都有多少条了.这些在glog中都是全局变量:
1
static
Mutex log_mutex;
2
// Number of messages sent at each severity. Under log_mutex.
3
int64 LogMessage::num_messages_[NUM_SEVERITIES] = {
0
,
0
,
0
,
0
};
当然,这里的num_messages_数组准确的说是LogMessage的静态成员变量,但是大体理解为全局变量也不为错,因为这个数据全局都只有一份了.
我自己以前也曾经做过日志系统,我的做法将这个系统作为一个Singleton类, 因为对绝大多数的系统而言, 日志输出系统都应该只有一份,然后使用这个单件进行操作.glog没有这样做,每条日志都是一个单独的LogMessage类对象,相互之间的影响不太多,其他的全局的资源都是全局对象.个人的分析,由于C++对单件类的实现支持很难,目前尚没有找到一个完全称得上高效而且绝对安全的实现方式(如果有,请告知,什么double check之类的就不要说了:),所以glog这种方式看上来精巧些.
2) CHECK_*宏的实现
glog有另一个强大的功能,也是很精巧,就是CHECK_*宏,它可以检测各种情况比如相等,大于小于等.这些功能其实很常见了,但是为什么说它精巧呢,看分析吧.
先来看这些宏的定义
1
#define CHECK_EQ(val1, val2) CHECK_OP(_EQ, ==, val1, val2)
2
#define CHECK_NE(val1, val2) CHECK_OP(_NE, !=, val1, val2)
3
#define CHECK_LE(val1, val2) CHECK_OP(_LE, <=, val1, val2)
4
#define CHECK_LT(val1, val2) CHECK_OP(_LT, < , val1, val2) #define CHECK_GE(val1, val2) CHECK_OP(_GE, >=, val1, val2)
5
#define CHECK_GT(val1, val2) CHECK_OP(_GT, > , val1, val2)
可以看到, 最后都会走到CHECK_OP这个宏里面:
1
#define CHECK_OP(name, op, val1, val2) \
2
CHECK_OP_LOG(name, op, val1, val2,
@ac_google_namespace
@::LogMessageFatal)
接着跟进CHECK_OP_LOG宏:
1
#define CHECK_OP_LOG(name, op, val1, val2, log) \
2
while
(
@ac_google_namespace
@::_Check_string* _result = \
3
@ac_google_namespace
@::Check##name##Impl( \
4
@ac_google_namespace
@::GetReferenceableValue(val1), \
5
@ac_google_namespace
@::GetReferenceableValue(val2), \
6
#val1
" "
#op
" "
#val2)) \
7
log(__FILE__, __LINE__, \
8
@ac_google_namespace
@::CheckOpString(_result)).stream()
可以看到, 这个宏的作用是判断某个条件,满足该条件则输出一条日志,log参数在这里对应的LogMessageFatal, 这个函数是输出一条信息之后让系统core掉.而Check##name##Impl这个宏,是根据不同的判断条件具体生成一个宏,如果是CHECK_EQ的话,对应的就是:
1
DEFINE_CHECK_OP_IMPL(_EQ, ==)
其中:
01
#define DEFINE_CHECK_OP_IMPL(name, op) \
02
template \
03
inline std::string* Check##name##Impl(
const
t1& v1,
const
t2& v2, \
04
const
char
* names) { \
05
if
(v1 op v2)
return
NULL; \
06
else
return
MakeCheckOpString(v1, v2, names); \
07
} \
08
inline std::string* Check##name##Impl(
int
v1,
int
v2,
const
char
* names) { \
09
return
Check##name##Impl(v1, v2, names); \
10
}
好了,终于到重点了,上面的代码中,关键的几句是:
1
if
(v1 op v2)
return
NULL; \
2
else
return
MakeCheckOpString(v1, v2, names); \
根据前面的情况, 如果v1 op v2为true, 则返回NULL;否则返回一个字符串, 从而调用log进行输出然后让系统core掉.
来看MakeCheckOpString的定义:
01
template
02
std::string* MakeCheckOpString(
const
t1& v1,
const
t2& v2,
const
char
* names) {
03
// It means that we cannot use stl_logging if compiler doesn't
04
// support using expression for operator.
05
// TODO(hamaji): Figure out a way to fix.
06
#
if
@ac_cv_cxx_using_operator
@
07
using ::operator<<;
08
#endif
09
std::strstream ss;
10
ss << names <<
" ("
<< v1 <<
" vs. "
<< v2 <<
")"
;
11
return
new
std::string(ss.str(), ss.pcount());
12
}
好了, 其实MakeCheckOpString的作用很简单啊,就是在CHECK_*检查失败的时候给出一条相对提示友好的输出信息罢了, 比如写CHECK_EQ(1, 2)的时候,这个CHECK显然是失败的,那么它给出的失败信息就是
“1 == 2 (1 vs. 2)”
其中”1 == 2″这个字符串对应的是
1
#define CHECK_OP_LOG(name, op, val1, val2, log) \
2
while
(
@ac_google_namespace
@::_Check_string* _result = \
3
@ac_google_namespace
@::Check##name##Impl( \
4
@ac_google_namespace
@::GetReferenceableValue(val1), \
5
@ac_google_namespace
@::GetReferenceableValue(val2), \
6
#val1
" "
#op
" "
#val2)) \
7
log(__FILE__, __LINE__, \
8
@ac_google_namespace
@::CheckOpString(_result)).stream()
中的”#val1 ” ” #op ” ” #val2″
分析完了,不得不说,glog中使用宏的地方确实多,精巧但是不仔细看看还是会看得很晕的,但是看明白了之后还是会赞一下作者的做法.
glog的功能不止这些,这两个部分只是我目前关注到的部分,以后可能还会做别的模块分析.