项目组一直使用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); |
这一次, 多了一个参数, 名为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 { |
03 | # pragma warning( default : 4275 ) |
06 | LogStream( char *buf, int len, int ctr) |
07 | : ostrstream(buf, len), |
11 | int ctr() const { return ctr_; } |
12 | void set_ctr( int ctr) { ctr_ = ctr; } |
13 | LogStream* self() const { return self_; } |
其实很简单, 从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() { |
Flush函数不详细分析了,主要做的事情就是将日志和之前得到的日期等进行格式化,最后调用注册的输出日志用的函数指针进行输出操作.
到此为止,一条glog日志就输出完成了.
再回头看看,实际上这里应该还有一些东西是需要全局使用的,比如有多条日志同时向一个文件进行输出的时候,需要对文件进行加锁,还比如要更新一些统计的数据,如每个级别的日志都有多少条了.这些在glog中都是全局变量:
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) \ |
03 | inline std::string* Check##name##Impl( const t1& v1, const t2& v2, \ |
05 | if (v1 op v2) return NULL; \ |
06 | else return MakeCheckOpString(v1, v2, names); \ |
08 | inline std::string* Check##name##Impl( int v1, int v2, const char * names) { \ |
09 | return Check##name##Impl(v1, v2, names); \ |
好了,终于到重点了,上面的代码中,关键的几句是:
1 | if (v1 op v2) return NULL; \ |
2 | else return MakeCheckOpString(v1, v2, names); \ |
根据前面的情况, 如果v1 op v2为true, 则返回NULL;否则返回一个字符串, 从而调用log进行输出然后让系统core掉.
来看MakeCheckOpString的定义:
02 | std::string* MakeCheckOpString( const t1& v1, const t2& v2, const char * names) { |
06 | # if @ac_cv_cxx_using_operator @ |
10 | ss << names << " (" << v1 << " vs. " << v2 << ")" ; |
11 | return new std::string(ss.str(), ss.pcount()); |
好了, 其实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的功能不止这些,这两个部分只是我目前关注到的部分,以后可能还会做别的模块分析.