豚鼠系列血肉之花在线: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的构造函数:

1LogMessage::LogMessage(const char* file, int line, LogSeverity severity) {2Init(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类中该流输入类的定义:

01class GOOGLE_GLOG_DLL_DECL LogStream : public std::ostrstream {02#ifdef _MSC_VER03# pragma warning(default4275)04#endif05public:06LogStream(char *buf, int len, int ctr)07: ostrstream(buf, len),08ctr_(ctr) {09self_ = this;10}11int ctr() const return ctr_; }12void set_ctr(int ctr) { ctr_ = ctr; }13LogStream* self() const return self_; }14private:15int ctr_;  // Counter hack (for the LOG_EVERY_X() macro)16LogStream *self_;  // Consistency check hack17};

其实很简单, 从std::ostrstream中继承过来,构造函数中有一个缓冲区就好了.在LogMessage::Init类中,定义该缓冲区的大小为:

1buf_ = new char[kMaxLogMessageLen+1];

其中

1const size_t LogMessage::kMaxLogMessageLen = 30000;

这里可以看到glog限制的一条日志长度大小为30000 byte.
其实我不明白为什么这样做, 如果每次打印一条日志都要分配一个这么大的缓冲区,而其实很多时候都是用不了这么大的,岂不是浪费了,难道说一次过分配足够大的内存可以有效率的提高,回头要尝试修改一下这个做法,不从std::ostrstream中继承自己管理缓冲区了,而是直接使用标准的std::ostringstream看看效率有没有大的变化.

好了,到此为止,流输入也有了,尽管往里面写数据就好.那么什么时候进行输出呢?我在今年早些时候,也曾经因为这个问题批判过C++, 由于对流输入操作结束位置判断方式的缺失,glog采用的是在这个创建的临时LogMessage对象(说它是”临时对象”是因为它是匿名的,因此不会保存,创建了就会被释放)被释放的时候,在析构函数中做输出操作:

1LogMessage::~LogMessage() {2Flush();3delete allocated_;4}

Flush函数不详细分析了,主要做的事情就是将日志和之前得到的日期等进行格式化,最后调用注册的输出日志用的函数指针进行输出操作.

到此为止,一条glog日志就输出完成了.
再回头看看,实际上这里应该还有一些东西是需要全局使用的,比如有多条日志同时向一个文件进行输出的时候,需要对文件进行加锁,还比如要更新一些统计的数据,如每个级别的日志都有多少条了.这些在glog中都是全局变量:

1static Mutex log_mutex;2// Number of messages sent at each severity.  Under log_mutex.3int64 LogMessage::num_messages_[NUM_SEVERITIES] = {0000};

当然,这里的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) \2CHECK_OP_LOG(name, op, val1, val2, @ac_google_namespace@::LogMessageFatal)

接着跟进CHECK_OP_LOG宏:

1#define CHECK_OP_LOG(name, op, val1, val2, log)                         \2while (@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))                                  \7log(__FILE__, __LINE__,                                             \8@ac_google_namespace@::CheckOpString(_result)).stream()

可以看到, 这个宏的作用是判断某个条件,满足该条件则输出一条日志,log参数在这里对应的LogMessageFatal, 这个函数是输出一条信息之后让系统core掉.而Check##name##Impl这个宏,是根据不同的判断条件具体生成一个宏,如果是CHECK_EQ的话,对应的就是:

1DEFINE_CHECK_OP_IMPL(_EQ, ==)

其中:

01#define DEFINE_CHECK_OP_IMPL(name, op) \02template  \03inline std::string* Check##name##Impl(const t1& v1, const t2& v2, \04const char* names) { \05if (v1 op v2) return NULL; \06else return MakeCheckOpString(v1, v2, names); \07} \08inline std::string* Check##name##Impl(int v1, int v2, const char* names) { \09return Check##name##Impl(v1, v2, names); \10}

好了,终于到重点了,上面的代码中,关键的几句是:

1if (v1 op v2) return NULL; \2else return MakeCheckOpString(v1, v2, names); \

根据前面的情况, 如果v1 op v2为true, 则返回NULL;否则返回一个字符串, 从而调用log进行输出然后让系统core掉.
来看MakeCheckOpString的定义:

01template02std::string* MakeCheckOpString(const t1& v1, const t2& v2, const char* names) {03// It means that we cannot use stl_logging if compiler doesn't04// support using expression for operator.05// TODO(hamaji): Figure out a way to fix.06#if @ac_cv_cxx_using_operator@07using ::operator<<;08#endif09std::strstream ss;10ss << names << " (" << v1 << " vs. " << v2 << ")";11return 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)                         \2while (@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))                                  \7log(__FILE__, __LINE__,                                             \8@ac_google_namespace@::CheckOpString(_result)).stream()

中的”#val1 ” ” #op ” ” #val2″

分析完了,不得不说,glog中使用宏的地方确实多,精巧但是不仔细看看还是会看得很晕的,但是看明白了之后还是会赞一下作者的做法.

glog的功能不止这些,这两个部分只是我目前关注到的部分,以后可能还会做别的模块分析.