- 后端
【免费下载链接】glog
C++ implementation of the Google logging module
本篇技术指南以 glog(Google Logging Library,C++ 实现的 Google 日志模块)的日志功能为主题,系统讲解其宏体系:如何按严重级别记录日志、控制日志文件位置与命名、理解日志行前缀格式并自定义前缀、使用条件/间歇日志、引入自定义 verbose 日志级别、利用调试模式日志、以 CHECK 系列宏做运行时检查,以及 PLOG、SYSLOG 等扩展日志形式。读完本文,你将掌握 glog 在 docs/logging.md 中定义的全部日志宏的语义、命令行控制方式与底层实现原理,可直接在真实 C++ 项目中落地使用。
严重级别(Severity Levels)
glog 定义了一组宏来简化常见日志任务。日志消息可以按严重级别(由低到高)分类:
INFOWARNINGERRORFATAL
记录一条FATAL级别的消息会终止程序(消息先被写出,然后进程退出)。在源码 src/glog/log_severity.h 中,这四个级别被定义为枚举值GLOG_INFO = 0、GLOG_WARNING = 1、GLOG_ERROR = 2、GLOG_FATAL = 3,并默认提供INFO、WARNING、ERROR、FATAL缩写别名;NUM_SEVERITIES固定为 4(见 src/glog/log_severity.h)。同文件注释给出了各级别的推荐语义:INFO用于状态变化或辅助调试;WARNING用于不希望发生但相对可预期的事件;ERROR用于程序可恢复的异常事件,且生产环境中的每个 ERROR 都应有 bug 可报;FATAL用于程序无法恢复的事件。
!!! note 某个级别的消息不仅会写入对应级别的日志文件,还会写入所有更低级别的日志文件。例如一条FATAL消息会同时出现在FATAL、ERROR、WARNING、INFO四个日志文件中(源码注释亦确认这一行为,见 src/glog/logging.h)。
DFATAL是一个特殊严重级别:在调试模式(即未定义NDEBUG宏)下记录FATAL级别的错误,在生产模式下自动降级为ERROR,从而避免程序被中断。其实现定义于 src/glog/log_severity.h:
#ifdef NDEBUG # define DFATAL_LEVEL ERROR #else # define DFATAL_LEVEL FATAL #endif日志文件(Log Files)
除非另行指定,glog 使用如下格式生成日志文件名:
<tmp>/<program name>.<hostname>.<user name>.log.<severity level>.<date>-<time>.<pid>其中<tmp>是存放日志文件的目录,其确定规则与平台相关。
Windows:glog 调用GetTempPathAAPI 函数获取临时文件目录,失败时依次回退到:
C:\TMP\C:\TEMP\
(按上述顺序)。对应实现见 src/logging.cc。
非 Windows:目录通过引用环境变量确定,依次为:
TMPDIRTMP
若均未设置,则回退到/tmp/(源码中还会优先读取仅测试用的TEST_TMPDIR,见 src/logging.cc)。
例如,Linux 上默认日志文件路径可能为:
/tmp/hello_world.example.com.hamaji.log.INFO.20080709-222411.10474默认情况下,glog 除写入日志文件外,还会把ERROR和FATAL消息回显到标准错误(stderr)。这对应于stderrthreshold旗标的默认行为(默认值为 2,即ERROR),相关旗标说明见 docs/flags.md。若安装了 gflags 库,可通过--log_dir=<dir>指定日志目录,或用环境变量GLOG_log_dir=<dir>达到同样效果。
日志行前缀格式(Log Line Prefix Format)
每条日志行的形式为:
Lyyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg...各字段含义如下:
| 占位符 | 含义 |
|---|---|
L | 单个字符,表示日志级别(如I表示INFO) |
yyyy | 年份 |
mm | 月份(零填充;如 5 月为05) |
dd | 日期(零填充) |
hh:mm:ss.uuuuuu | 时:分:秒.微秒 |
threadid | 空格填充的线程 ID |
file | 文件名 |
line | 行号 |
msg | 用户提供的消息内容 |
!!! example "默认日志行前缀格式"I1103 11:57:31.739339 24395 google.cc:2341] Command line: ./some_prog I1103 11:57:31.739403 24395 google.cc:2342] Process id 24395
!!! note 虽然微秒精度对同一台机器上的事件排序很有用,但不同机器的时钟未必同步。因此,在比较不同机器时间戳的低位比特时要谨慎。
自定义前缀格式(Format Customization)
预定义的日志行前缀可以用用户提供的回调函数替换。每条日志记录产生时,回调会收到一个包含严重级别、文件名、行号、线程 ID 与事件时间的google::LogMessage实例引用,以及输出流引用——输出流中的内容会被前置到最终日志行的实际消息之前。
启用前缀格式化器,需在初始化阶段调用:
google::InstallPrefixFormatter(&MyPrefixFormatter);InstallPrefixFormatter接受第二个可选参数,类型为void*,用于向回调传递用户数据(声明见 src/glog/logging.h,实现见 src/logging.cc)。
!!! example "自定义前缀格式化器" 下面的函数输出的前缀与 glog 默认格式完全一致。第三个参数data用于访问用户数据,未指定时默认为nullptr:
``` cpp void MyPrefixFormatter(std::ostream& s, const google::LogMessage& m, void* /*data*/) { s << google::GetLogSeverityName(m.severity())[0] << setw(4) << 1900 + m.time().year() << setw(2) << 1 + m.time().month() << setw(2) << m.time().day() << ' ' << setw(2) << m.time().hour() << ':' << setw(2) << m.time().min() << ':' << setw(2) << m.time().sec() << "." << setw(6) << m.time().usec() << ' ' << setfill(' ') << setw(5) << m.thread_id() << setfill('0') << ' ' << m.basename() << ':' << m.line() << "]"; } ```从源码结构看(src/glog/logging.h),LogMessageTime类型提供了year()、month()、day()、hour()、min()、sec()、usec()等时间访问接口,回调可通过m.time()获取事件时间;m.severity()、m.thread_id()、m.basename()、m.line()分别返回级别、线程 ID、文件名(不含路径)与行号。
条件日志与间歇日志(Conditional / Occasional Logging)
有时你可能只想在特定条件下记录日志。glog 提供以下宏:
LOG_IF(INFO, num_cookies > 10) << "Got lots of cookies";当变量num_cookies大于 10 时,才会记录 "Got lots of cookies"。如果某行代码被执行很多次,按固定间隔记录日志会很有用,这类日志最适合信息性消息:
LOG_EVERY_N(INFO, 10) << "Got the " << google::COUNTER << "th cookie";上面这行在第 1、11、21……次执行时输出日志消息。
!!! note 占位符google::COUNTER用于标识当前是第几次重复(对应宏实现中的次数计数器,见 src/glog/logging.h)。
可以将条件与间歇日志结合:
LOG_IF_EVERY_N(INFO, (size > 1024), 10) << "Got the " << google::COUNTER << "th big cookie";除了每隔第 n 次输出外,也可以限制只输出前 n 次:
LOG_FIRST_N(INFO, 20) << "Got the " << google::COUNTER << "th cookie";上面这行只在前 20 次执行时输出日志消息,google::COUNTER标识当前是第几次重复。
还有些场景希望按时间周期输出,例如每 10 毫秒记录一次:
LOG_EVERY_T(INFO, 0.01) << "Got a cookie";或每 2.35 秒一次:
LOG_EVERY_T(INFO, 2.35) << "Got a cookie";从实现层面看(src/glog/logging.h),LOG_EVERY_N系列宏会在调用点生成两个static std::atomic<int>计数器(occurrences_与occurrences_mod_n_),通过取模判断当前次数是否为第 1 次命中;LOG_EVERY_T则基于std::chrono::steady_clock的单调时钟计算时间差,超过指定周期才输出;LOG_FIRST_N使用一个static std::atomic<int>计数器记录已输出次数。由于这些计数是线程安全的原子变量,宏可在多线程环境下使用,并且与__LINE__拼接的静态变量名保证了每个调用点是独立的计数器。
详细日志(Verbose Logging)
排查复杂 bug 时,详尽日志非常有用;但日常开发中你可能希望忽略过于啰嗦的消息。glog 为此提供VLOG宏,允许你定义自己的数字日志级别。
--v命令行选项控制哪些 verbose 消息会被记录:
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,verbose 级别越低,消息越容易被记录。例如--v=1时,VLOG(1)会记录,而VLOG(2)不会。
!!! warningVLOG的行为与严重级别日志正好相反:INFO、ERROR等是递增定义的,因此--minloglevel=1只会记录WARNING及以上级别。
VLOG宏与--v旗标虽然可以指定任意整数,但通常使用小的正整数。例如写了VLOG(0),则需要--v=-1或更低才能将其静音——这在大多数场景下并不实用,因为我们通常不希望默认输出 verbose 日志。VLOG宏只要记录,就总是在INFO日志级别输出(见 src/glog/logging.h 中#define VLOG(verboselevel) LOG_IF(INFO, VLOG_IS_ON(verboselevel))的定义)。
verbose 日志还可以按模块从命令行控制:
--vmodule=mapreduce=2,file=1,gfs*=3 --v=0这些选项的精确效果是:
- 输出
mapreduce.{h,cc}中的VLOG(2)及更低级别消息 - 输出
file.{h,cc}中的VLOG(1)及更低级别消息 - 输出文件名以 "gfs" 开头的文件中的
VLOG(3)及更低级别消息 - 输出其余位置的
VLOG(0)及更低级别消息
其中第 3 点的通配符功能支持*(匹配 0 个或多个字符)和?(匹配任意单个字符)两种通配符。更多命令行旗标说明请参考 docs/flags.md。
在实现层面,--vmodule的解析与匹配发生在VLOG_IS_ON宏中(src/glog/vlog_is_on.h):GCC 下每个VLOG_IS_ON(n)调用点会生成一个匿名静态SiteFlag变量,首次执行时通过InitVLOG3__根据源文件名匹配--vmodule规则(文件名匹配时会忽略-inl后缀,见 src/glog/vlog_is_on.h),后续直接读取对应的级别变量判断是否输出;非 GCC 编译器不支持--vmodule,仅由FLAGS_v控制(VLOG_IS_ON(verboselevel)退化为(FLAGS_v >= (verboselevel)))。
另外还有VLOG_IS_ON(n)"verbose 级别"条件宏,当--v大于等于n时返回true。用法如下:
if (VLOG_IS_ON(2)) { // (1) }- 这里可以做某些日志准备工作,以及单纯用
VLOG(2) << "message ...";无法完成的日志记录。
verbose 级别条件宏VLOG_IF、VLOG_EVERY_N和VLOG_IF_EVERY_N的行为与LOG_IF、LOG_EVERY_N、LOG_IF_EVERY_N类似,只是接受数字 verbose 级别而非严重级别:
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 occurrence is " << google::COUNTER; VLOG_IF_EVERY_N(1, (size > 1024), 10) << "I’m printed on every 10th occurrence of case when size is more " " than 1024, when you run the program with --v=1 or more. "; "Present occurrence is " << google::COUNTER;这些宏在 src/glog/logging.h 中被实现为LOG_IF(INFO, ...)、LOG_IF_EVERY_N(INFO, ...)的组合,因此同样会短路求值。
若需要运行时动态控制某模块的 verbose 级别,可使用google::SetVLOGLevel(module_pattern, log_level)辅助函数,它能在程序运行期覆盖--vmodule的配置(声明见 src/glog/vlog_is_on.h)。
!!! info "性能" glog 提供的条件日志宏(如CHECK、LOG_IF、VLOG等)经过精心实现,条件为假时不会执行右值表达式。因此下面的检查不会牺牲应用程序性能:
``` cpp CHECK(obj.ok) << obj.CreatePrettyFormattedStringButVerySlow(); ```调试支持(Debugging Support)
调试模式专用的日志宏只在调试模式下生效,非调试编译时会被完全编译掉。使用这些宏可以避免因日志过多拖慢生产应用:
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"; DLOG_FIRST_N(INFO, 10) << "Got the " << google::COUNTER << "th cookie"; DLOG_EVERY_T(INFO, 0.01) << "Got a cookie";从源码看(src/glog/logging.h),这些宏由DCHECK_IS_ON()控制:调试模式(未定义NDEBUG且未定义DCHECK_ALWAYS_ON)下直接展开为对应的LOG系列宏;生产模式下展开为static_cast<void>(0)之类的空操作,表达式根本不会被求值。类似地,DVLOG在非调试模式下也会被编译掉。
运行时检查(Runtime Checks)
频繁检查程序中的预期条件、尽早发现错误是良好的实践。CHECK宏提供与 C 标准库assert类似的能力:当条件不满足时终止应用程序。
CHECK在条件不为真时终止程序。与assert不同,它不受NDEBUG控制,无论何种编译模式都会执行检查。因此,下面例子中的fp->Write(x)总是会被执行:
CHECK(fp->Write(x) == 4) << "Write failed!";实现上CHECK等价于LOG_IF(FATAL, GOOGLE_PREDICT_BRANCH_NOT_TAKEN(!(condition)))(见 src/glog/logging.h),并借助__builtin_expect分支预测提示编译器失败分支极不可能发生。
glog 还提供一系列相等/不等检查的辅助宏:CHECK_EQ、CHECK_NE、CHECK_LE、CHECK_LT、CHECK_GE、CHECK_GT。它们比较两个值,当结果不符合预期时记录一条包含两个值的FATAL消息。参与比较的值必须定义了operator<<(ostream, ...)。
可以像这样在错误消息后追加内容:
CHECK_NE(1, 2) << ": The world must be ending!";glog 的实现非常谨慎地保证每个参数恰好被求值一次,任何合法作为函数参数的表达式在这里都合法。特别地,参数可以是临时表达式,在语句结束时才被销毁,例如:
CHECK_EQ(string("abc")[1], ’b’);如果两个参数中一个是指针、另一个是nullptr,编译器会报错。解决办法是把nullptr用static_cast转换到目标指针类型:
CHECK_EQ(some_ptr, static_cast<SomeType*>(nullptr));更好的做法是使用CHECK_NOTNULL宏:
CHECK_NOTNULL(some_ptr); some_ptr->DoSomething();由于该宏会返回传入的指针,在构造函数初始化列表中使用非常方便:
struct S { S(Something* ptr) : ptr_(CHECK_NOTNULL(ptr)) {} Something* ptr_; };!!! warning 由于参数转发机制,CHECK_NOTNULL不能同时流式输出额外的自定义消息。如需自定义消息,可在失败检查之前使用CHECK_EQ宏。
如果比较的是 C 字符串(char *),glog 提供一组同时支持大小写敏感与不敏感比较的宏:CHECK_STREQ、CHECK_STRNE、CHECK_STRCASEEQ、CHECK_STRCASENE。CHECK_*CASE*变体不区分大小写。这些宏可以安全地传入nullptr指针:nullptr与任何非nullptr字符串视为不相等,两个nullptr视为相等。
!!! note 两个参数都可以是在当前完整表达式结束时销毁的临时对象,例如:
``` cpp CHECK_STREQ(Foo().c_str(), Bar().c_str()); ``` 其中 `Foo` 和 `Bar` 返回 `std::string`。CHECK_DOUBLE_EQ宏检查两个浮点值的相等性,允许一个小的误差余量。CHECK_NEAR接受第三个浮点参数,用于指定可接受的误差余量。二者实现于 src/glog/logging.h:CHECK_DOUBLE_EQ等价于以 ±1e-15 为界的CHECK_LE/CHECK_GE组合;CHECK_NEAR(val1, val2, margin)则要求两值之差在 ±margin之内。同文件还定义了CHECK_INDEX(I, A)与CHECK_BOUND(B, A)用于数组越界检查。
原始日志(Raw Logging)
头文件<glog/raw_logging.h>提供线程安全的日志记录,它不分配任何内存、不获取任何锁。因此,该头文件中定义的宏可以用于底层内存分配与同步代码。详见 src/glog/raw_logging.h。
该头文件的特点(从注释与实现看,src/glog/raw_logging.h)包括:仅直接、无缓冲地写入 stderr;使用显式的格式与参数列表(printf 风格);超长消息会被静默截断。用法示例:
RAW_LOG(ERROR, "Failed foo with %i: %s", status, error); RAW_VLOG(3, "status is %i", status);输出形如(仅写至 stderr):
E20200821 211317 file.cc:123] RAW: Failed foo with 22: bad_file I20200821 211317 file.cc:142] RAW: status is 20RAW_VLOG同样受VLOG_IS_ON与STRIP_LOG控制(见 src/glog/raw_logging.h),可通过STRIP_LOG编译期完全剥离日志代码。
Google 风格perror():PLOG / PCHECK
PLOG()、PLOG_IF()和PCHECK()的行为与对应的LOG*和CHECK完全一致,区别在于它们会在输出行末尾追加对当前errno状态的描述。例如:
PCHECK(write(1, nullptr, 2) >= 0) << "Write nullptr failed";该检查会失败,并输出如下错误消息:
F0825 185142 test.cc:22] Check failed: write(1, nullptr, 2) >= 0 Write nullptr failed: Bad address [14]从实现看(src/glog/logging.h),PLOG系列使用ErrnoLogMessage派生类,其析构函数会追加": strerror(errno) [errno]"后缀(见 src/glog/logging.h)。此外还有CHECK_ERR(invocation)宏,专门用于断言返回 -1 并设置errno的函数的成功调用,例如:
CHECK_ERR(mkdir(path, 0700));或:
int fd = open(filename, flags); CHECK_ERR(fd) << ": open " << filename;Syslog
glog 提供SYSLOG、SYSLOG_IF和SYSLOG_EVERY_N宏。它们除了写入正常日志外,还会写入 syslog。请注意:写入 syslog 可能显著影响性能,尤其是当 syslog 被配置为远程日志记录时!使用这些宏之前务必理解输出到 syslog 的影响。一般而言,建议克制地使用这些宏。
在源码中,SYSLOG系列通过LogMessage::SendToSyslogAndLog发送方法实现(见 src/glog/logging.h),SYSLOG_EVERY_N则将LOG_EVERY_N的发送方法替换为SendToSyslogAndLog(见 src/glog/logging.h)。
相关文档延伸
- docs/flags.md:
logtostderr、stderrthreshold、minloglevel、log_dir、v、vmodule等输出控制旗标的完整说明,以及通过 gflags 命令行参数、GLOG_前缀环境变量和FLAGS_*全局变量修改旗标的方式。 - docs/usage.md:在 CMake 工程中通过
find_package(glog)或add_subdirectory引入 glog,并链接glog::glog目标。 - src/glog/logging.h 与 src/glog/vlog_is_on.h:本文所述全部宏的定义与实现细节。
- 后端
【免费下载链接】glog
C++ implementation of the Google logging module
相关推荐
SwiftLog条件日志:基于运行时条件的日志记录
SwiftLog条件日志:基于运行时条件的日志记录 在日常开发中,你是否遇到过这些问题:生产环境中调试日志过多影响性能,关键操作却没有详细日志,或者需要根据用户
FreshRSS 日志与错误排查完全指南:日志文件位置、日志级别配置与故障定位
FreshRSS 日志与错误排查完全指南:日志文件位置、日志级别配置与故障定位 导读 本文是 FreshRSS 管理员排查故障的核心手册,系统讲解 FreshR
后端前端CLI从ExoPlayer 2.X到Media3:技术架构升级的3大战略优势与迁移实施指南
从ExoPlayer 2.X到Media3:技术架构升级的3大战略优势与迁移实施指南 随着Android媒体生态的演进,ExoPlayer 2.X的技术架构已无
音视频移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考