glog文档

来自Google的Glog是一个应用程序的日志库。它提供基于C++风格的流的日志API,以及各种辅助的宏。打印日志只需以流的形式传给LOG(level),例如:

#include <glog/logging.h>
int main(int argc, char* argv[]) {
    // Initialize Google's logging library.
    google::InitGoogleLogging(argv[0]);

    // ...
    LOG(INFO) << "Found " << num_cookies << " cookies";
}

// 编译和运行
// g++ test.cpp -lglog -lpthread -o test

Glog定义了一系列的宏来简化记录日志的工作。你可以按级别打印日志,通过命令行控制日志行为,按条件打印日志,不满足条件时终止程序,引入自定义的日志级别,等等。

日志级别

可以指定下面这些级别(按严重性递增排序):INFO,WARNING,ERROR和FATAL。打印FATAL消息会在打印完成后终止程序。和其他日志库类似,级别更高的日志会在同级别和所有低级别的日志文件中打印。DFATAL级别会在调试模式(没有定义NDEBUG宏)中打印FATAL日志,但是会自动降级为ERROR级别,而不终止程序。

如果不指定的话,Glog输出到文件/tmp/<programname>.<hostname>.<username>.log.<severitylevel>.<date>-<time>.<pid>(比如/tmp/hello_world.example.com.hamaji.log.INFO.20080709-222411.10474)。默认情况下,Glog对于ERROR和FATAL级别的日志会同时输出到stderr。

设置flag

一些flag会影响Glog的输出行为。如果安装了GFlags库,编译时会默认使用它,这样就可以在命令行传递flag(别忘了调用ParseCommandLineFlags初始化)。比如你想打开--logtostderrflag,可以这么用:

./your_application --logtostderr=1

如果没有安装GFlags,那可以通过环境变量来设置,在flag名前面加上前缀GLOG_。比如:

GLOG_logtostderr=1 ./your_application

常用的flag有:

  • logtostderr(bool,默认为false):日志输出到stderr,不输出到日志文件。
  • colorlogtostderr(bool,默认为false):输出彩色日志到stderr。
  • stderrthreshold(int,默认为2,即ERROR):将大于等于该级别的日志同时输出到stderr。日志级别INFO,WARNING,ERROR,FATAL的值分别为0、1、2、3。
  • minloglevel(int,默认为0,即INFO):打印大于等于该级别的日志。日志级别的值同上。
  • log_dir(string,默认为""):指定输出日志文件的目录。
  • v(int,默认为0):显示所有VLOG(m)的日志,m小于等于该flag的值。会被--vmodule覆盖。
  • vmodule(string,默认为""):每个模块的详细日志的级别。参数为逗号分隔的一组=支持通配(即gfs*代表所有gfs开头的名字),匹配不包含扩展名的文件名(忽略.cc/.h./-inl.h等)。会覆盖--v指定的值。

logging.cc中还定义了其他一些flag。grep一下DEFINE_可以看到全部。也可以通过修改FLAGS_*全局变量来改变flag的值。

LOG(INFO) << "file";
// Most flags work immediately after updating values.
FLAGS_logtostderr = 1;
LOG(INFO) << "stderr";
FLAGS_logtostderr = 0;
// This won't change the log destination. If you want to set this
// value, you should do this before google::InitGoogleLogging .
FLAGS_log_dir = "/some/log/directory";
LOG(INFO) << "the same file";

按条件/次数打印日志

有时你可能只想在满足一定条件的时候打印日志。可以使用下面的宏来按条件打印日志:

LOG_IF(INFO, num_cookies > 10) << "Got lots of cookies";

上面的日志只有在满足num_cookies>10时才会打印。另一种情况,如果代码被执行多次,可能只想对其中某几次打印日志。

LOG_EVERY_N(INFO, 10) << "Got the " << google::COUNTER << "th cookie";

上面的代码会在执行的第1、11、21、...次时打印日志。google::COUNTER用来表示是哪一次执行。

可以将这两种日志用下面的宏合并起来。

LOG_IF_EVERY_N(INFO, (size > 1024), 10) << "Got the " << google::COUNTER << "th big cookie";

不只是每隔几次打印日志,也可以限制在前n次打印日志:

LOG_FIRST_N(INFO, 20) << "Got the " << google::COUNTER << "th cookie";

上面会在执行的前20次打印日志。

调试模式

调试模式的日志宏只在调试模式下有效,在非调试模式会被清除。可以避免生产环境的程序由于大量的日志而变慢。

DLOG(INFO) << "Found cookies";
DLOG_IF(INFO, num_cookies > 10) << "Got lots of cookies";
DLOG_EVERY_N(INFO, 10) << "Got the " << google::COUNTER << "th cookie";

CHECK宏

常做状态检查以尽早发现错误是一个很好的编程习惯。CHECK宏和标准库中的assert宏类似,可以在给定的条件不满足时终止程序。CHECK和assert不同的是,它不由NDEBUG控制,所以一直有效。因此下面的fp->Write(x)会一直执行:

CHECK(fp->Write(x) == 4) << "Write failed!";

有各种用于相等/不等检查的宏:CHECK_EQ,CHECK_NE,CHECK_LE,CHECK_LT,CHECK_GE,CHECK_GT。它们比较两个值,在不满足期望时打印包括这两个值的FATAL日志。注意这里的值需要定义了operator<<(ostream,...)。

比如:

CHECK_NE(1, 2) << ": The world must be ending!";

每个参数都可以保证只用一次,所以任何可以做为函数参数的都可以传给它。参数也可以是临时的表达式,比如:

CHECK_EQ(string("abc")[1], 'b');

如果一个参数是指针,另一个是NULL,编译器会报错。可以给NULL加上对应类型的static_cast来绕过。

CHECK_EQ(some_ptr, static_cast<SomeType*>(NULL));

更好的办法是用CHECK_NOTNULL宏:

CHECK_NOTNULL(some_ptr);
some_ptr->DoSomething();

该宏会返回传入的指针,因此在构造函数的初始化列表中非常有用。

struct S {
    S(Something* ptr) : ptr_(CHECK_NOTNULL(ptr)) {}
    Something* ptr_;
};

因为该特性,这个宏不能用作C++流。如果需要额外信息,请使用CHECK_EQ。

如果是需要比较C字符串(char*),可以用CHECK_STREQ,CHECK_STRNE,CHECK_STRCASEEQ,CHECK_STRCASENE。CASE的版本是不区分大小写的。这里可以传入NULL。NULL和任何非NULL的字符串是不等的,两个NULL是相等的。这里的参数都可以是临时字符串,比如CHECK_STREQ(Foo().c_str(),Bar().c_str())。

CHECK_DOUBLE_EQ宏可以用来检查两个浮点值是否等价,允许一点误差。CHECK_NEAR还可以传入第三个浮点参数,指定误差。

细节日志

当你在追比较复杂的bug的时候,详细的日志信息非常有用。但同时,在通常开发中需要忽略太详细的信息。对这种细节日志的需求,Glog提供了VLOG宏,使你可以自定义一些日志级别。通过--v可以控制输出的细节日志:

VLOG(1) << "I'm printed when you run the program with --v=1 or higher";
VLOG(2) << "I'm printed when you run the program with --v=2 or higher";

和日志级别相反,级别越低的VLOG越会打印。比如--v=1的话,VLOG(1)会打印,VLOG(2)则不会打印。对VLOG宏和--vflag可以指定任何整数,但通常使用较小的正整数。VLOG的日志级别是INFO。

细节日志可以控制按模块输出:

--vmodule=mapreduce=2,file=1,gfs*=3 --v=0

会:

  • 为mapreduce.{h,cc}打印VLOG(2)和更低级别的日志
  • 为file.{h,cc}打印VLOG(1)和更低级别的日志
  • 为前缀为gfs的文件打印VLOG(3)和更低级别的日志
  • 其他的打印VLOG(0)和更低级别的日志
  • 其中©给出的通配功能支持*(0或多个字符)和?(单字符)通配符。

细节级别的条件判断宏VLOG_IS_ON(n)当--v大于等于n时返回true。比如:

if (VLOG_IS_ON(2)) {
    // do some logging preparation and logging
    // that can't be accomplished with just VLOG(2) << ...;
}

此外还有VLOG_IF,VLOG_EVERY_N,VLOG_IF_EVERY_N和LOG_IF,LOG_EVERY_N,LOF_IF_EVERY类似,但是它们传入的是一个数字的细节级别。

VLOG_IF(1, (size > 1024)) << "I'm printed when size is more than 1024 and when you run the program with --v=1 or more";
VLOG_EVERY_N(1, 10) << "I'm printed every 10th occurrence, and when you run the program with --v=1 or more. Present occurence is " << google::COUNTER;
VLOG_IF_EVERY_N(1, (size > 1024), 10)
   << "I'm printed on every 10th occurence of case when size is more "
      " than 1024, when you run the program with --v=1 or more. ";
      "Present occurence is " << google::COUNTER;

失败信号处理

Glog库还提供了一个信号处理器,能够在SIGSEGV之类的信号导致的程序崩溃时导出有用的信息。使用google::InstallFailureSignalHandler()加载信号处理器。下面是它输出的一个例子。

*** Aborted at 1225095260 (unix time) try "date -d @1225095260" if you are using GNU date ***
*** SIGSEGV (@0x0) received by PID 17711 (TID 0x7f893090a6f0) from PID 0; stack trace: ***
PC: @           0x412eb1 TestWaitingLogSink::send()
    @     0x7f892fb417d0 (unknown)
    @           0x412eb1 TestWaitingLogSink::send()
    @     0x7f89304f7f06 google::LogMessage::SendToLog()
    @     0x7f89304f35af google::LogMessage::Flush()
    @     0x7f89304f3739 google::LogMessage::~LogMessage()
    @           0x408cf4 TestLogSinkWaitTillSent()
    @           0x4115de main
    @     0x7f892f7ef1c4 (unknown)
    @           0x4046f9 (unknown)

注意:InstallFailureSignalHandler()在x86_64系统架构上可能会引发退栈的死锁,导致递归地调用malloc。这是内置的退栈的bug,建议在安装Glog之前安装libunwind。更多解释可以看Glog的INSTALL文件。

# apt-get install libunwind libunwind-dev

默认情况,信号处理器把失败信息导出到stderr。可以用InstallFailureWriter()定制输出位置。

其他

支持CMake

Glog并不自带CMake支持,如果想在CMake脚本中使用它,可以把FindGlog.cmake添加到CMake的模块目录下。然后像下面这样使用:

find_package (Glog REQUIRED)
include_directories (${GLOG_INCLUDE_DIR})
add_executable (foo main.cc)
target_link_libraries (foo glog)

性能

Glog提供的条件日志宏(比如CHECK,LOG_IF,VLOG,...)在条件判断失败时,不会执行右边表达式。因此像下面这样的检查不会牺牲程序的性能。

CHECK(obj.ok) << obj.CreatePrettyFormattedStringButVerySlow();

自定义失败处理函数

FATAL级别的日志和CHECK条件失败时会终止程序。可以用InstallFailureFunction改变该行为。

void YourFailureFunction() {
    // Reports something...
    exit(1);
}

int main(int argc, char* argv[]) {
    google::InstallFailureFunction(&YourFailureFunction);
}

默认地,Glog会导出stacktrace,程序以状态1退出。stacktrace只在Glog支持栈跟踪的系统架构(x86和x86_64)上导出。

原始日志

可用于要求线程安全的日志,它不分配任何内存,也不加锁。因此,该头文件中定义的宏可用于底层的内存分配和同步的代码。

谷歌风格的perror()

PLOG(),PLOG_IF(),PCHECK()和对应的LOG*和CHECK类似,但它们会同时在输出中加上当前errno的描述。如:

PCHECK(write(1, NULL, 2) >= 0) << "Write NULL failed";

下面是它的输出:

F0825 185142 test.cc:22] Check failed: write(1, NULL, 2) >= 0 Write NULL failed: Bad address [14]
Syslog

SYSLOG,SYSLOG_IF,SYSLOG_EVERY_N宏会在正常日志输出的同时输出到syslog。注意输出日志到syslog会大幅影响性能,特别是如果syslog配置为远程日志输出。所以在用它们之前一定要确定影响,一般来说很少使用。

跳过日志

打印日志的代码中的字符串会增加可执行文件的大小,而且也会带来泄密的风险。可以通过使用GOOGLE_STRIP_LOG宏来删除所有低于特定级别的日志:

比如使用下面的代码:

#define GOOGLE_STRIP_LOG 1    // this must go before the #include!
#include <glog/logging.h>

编译器会删除所有级别低于该值的日志。因为VLOG的日志级别是INFO(等于0),设置GOOGLE_STRIP_LOG大于等于1会删除所有VLOG和INFO日志。

修改输出格式

修改src/logging.cc文件:

    stream() << "["
             << setw(4) << 1900+ data_->tm_time_.tm_year << "-"
                 << setw(2) << 1+data_->tm_time_.tm_mon << "-"
                 << setw(2) << data_->tm_time_.tm_mday << " "
                 << setw(2) << data_->tm_time_.tm_hour  << ':'
                 << setw(2) << data_->tm_time_.tm_min   << ':'
                 << setw(2) << data_->tm_time_.tm_sec   << "."
                 << setw(6) << usecs
                 << ' '
                 << setfill(' ') << setw(5)
                 << static_cast<unsigned int>(GetTID()) << setfill('0')
                 << ' ' << LogSeverityNames[severity] << " "
                 << data_->basename_ << ':' << data_->line_ << "] ";
      }
      data_->num_prefix_chars_ = data_->stream_.pcount();