【C++三方组件】glog:Google 出品的 C++ 日志库
【摘要】:spdlog 之前十年的世界:
LOG(INFO) << "msg";------流式语法、I/W/E 级别前缀、文件行号自动附体。glog 三件套是 LOG 分级、VLOG 按号码细控、CHECK 断言家族(实测失败即 F 级报文Check failed: retries <= 10 (99 vs. 10)+ 堆栈 + 非零退出)。Why 拆三问:流式语法为什么是那一代的选择、VLOG 的号码比级别好在哪、CHECK 为什么比 assert 更适合生产。与 gflags 的黄金搭档收尾。【版本基准】:glog 0.7.1(BSD-3)|C++17|文中输出均为 g++ 13.1 实测
(一句话定位:glog 是「C++ 日志词汇表的立法者」------哪怕你最终用 spdlog,说的仍是它定的词。)
1. What:上一代的日志标准
glog 从 Google 内部走出(2010s 开源),设计带着鲜明的时代与工程烙印:日志是宏、级别是大写字母、断言是一等公民 。它不追接口优雅,追的是「百万行代码库里每个 LOG 都便宜、每条日志自带源位置、每个不变量都能一行声明」。
今天的新项目默认会选 spdlog(下一篇),但 glog 仍是理解存量代码与 Google 系生态(gflags、abseil logging)的必修课。
源码集成时与 gflags 的组合值得单独说:-DWITH_GFLAGS=ON 编出的 glog 会把全部 FLAGS_ 注册成 gflags 标志------日志参数从此走 gflags 的收割与 help 体系 (本篇实测为独立构建,FLAGS_ 直接可用,两种姿势的 FLAGS_ 行为一致、来源不同)。混合栈的团队常选组合构建,让 --help 一屏看全。
(接入姿势与本篇实测对齐:CMake target 链接、main 首行 Init、FLAGS_ 直调。)
2. 项目接入
json
// vcpkg:vcpkg.json
{ "dependencies": [ "glog" ] }
cmake
find_package(glog CONFIG REQUIRED)
target_link_libraries(app PRIVATE glog::glog)
编译库(本篇实测即本地构建);注意用 CMake target 接入------手工链静态库需要正确的导出宏配置(实测直连报「not included correctly」)。
输出格式的字段逐个点评(实测行 I20260918 01:30:24.192250 1 demo_glog.cpp:6]):首字母级别(I/W/E/F)------grep 一发命中;微秒时间戳------对齐多机日志的时钟轴;线程号(第 4 列的 1)------并发归因;文件:行 ------直达现场;] 后正文。一行 60 字节回答了「何时何线程哪个分支说了什么」------grep 友好是格式设计的第一目标,这个审美统治了服务端日志二十年。
3. 核心概念:宏 + 流 + 自动源位置
cpp
google::InitGoogleLogging(argv[0]); // 必须最先调用
FLAGS_logtostderr = 1; // 直写 stderr(开发常态)
LOG(INFO) << "info msg " << 42;
LOG(WARNING) << "warn msg";
LOG(ERROR) << "error msg";
三个特征一眼可辨:流式语法 (<< 拼接,那一代的惯用法);每行自带 文件:行号 (宏展开时抓取,零成本);级别是前缀字母(I/W/E/F)。实测输出:
text
I20260918 01:30:24.192250 1 demo_glog.cpp:6] info msg 42
W20260918 01:30:24.193006 1 demo_glog.cpp:7] warn msg
E20260918 01:30:24.193008 1 demo_glog.cpp:8] error msg
常用 FLAGS 速查(与 gflags 联动,可命令行直调):logtostderr(开发期直打屏)、v(VLOG 号码阈值)、log_dir(文件模式输出目录)、alsologtostderr(文件 + 屏双路)、minloglevel(全局静音,0 全开 3 只 fatal)、logbufsecs(缓冲刷新间隔)。运维排障的「三板斧」就是 --v=2 --logtostderr=1 --log_dir=/tmp------三个标志不用改代码。
CHECK 家族的完整清单按用途分三组:等式比较 (CHECK_EQ/NE/LT/LE/GT/GE,两侧值打进报文------实测的 (99 vs. 10));状态断言 (CHECK(ptr)、CHECK_NOTNULL(ptr)------顺带返回指针可直接用在表达式里);前置与不变量 (DCHECK_* 系列是 debug-only 版本------release 蒸发,给「昂贵的断言」留的口子,与 assert 的 NDEBUG 语义对齐但命名显式)。选型口诀:不变量用 CHECK(永不蒸发)、热路径昂贵检查用 DCHECK。
(实测输出三例:I/W/E 分级行、VLOG(1) 随 FLAGS_v 显隐、CHECK_LE 失败的 F 级报文与非零退出。)
4. How:VLOG 细控与 CHECK 断言(实测)
VLOG------比级别更细的开关 。级别只有四档,而「这个模块想多打一点」的需求是连续的------glog 的答案是用号码 :VLOG(1)、VLOG(2)......默认全关,FLAGS_v = N 打开所有号码 ≤ N 的:
cpp
FLAGS_v = 1;
VLOG(1) << "vlog1 now visible";
VLOG(2) << "vlog2 still hidden";
// 实测:第 1 行输出,第 2 行沉默
生产常年关着,排障时按号逐级打开------日志的「音量旋钮」从四档变成了无级。
CHECK------生产级断言 。assert 的问题:NDEBUG 一开就蒸发。CHECK 家族(CHECK_EQ/LE/...)永远在,失败时打 F 级日志、附堆栈、然后退出:
cpp
int retries = 99;
CHECK_LE(retries, 10) << "retries out of range: " << retries;
实测(单独进程运行):
text
F20260918 01:30:39.313477 1 demo_glog_check.cpp:6]
Check failed: retries <= 10 (99 vs. 10)
retries out of range: 99
@ 00007ff783f14c3a ← 附调用栈地址
(进程非零退出)
Check failed: retries <= 10 (99 vs. 10)------表达式、两侧实值 都在,配 << 的补充说明------一条报错就是一份现场报告。
时间线视角补一段:glog 之后 Google 内部演化出了 abseil 的 logging( absl::Log 家族),glog 本身进入维护态(本篇实测 0.7.1 仍在修)------但它确立的词汇表 (VLOG 的号码、CHECK 家族、文件名行号前缀)已成为 C++ 日志的通用语,后来的库(包括 spdlog 的级别讨论、absl 的 LOG 接口)都在这套词汇上说话。学 glog 的最大回报不是用它,是读懂这套词汇。
VLOG 的「按模块」细控展开(文档级):--vmodule=sync=3,db=1 让 sync 文件的 VLOG 开到 3、db 开到 1------模块名的匹配规则是文件名(不含扩展名),粒度恰好落在「一个 .cpp 一个关注点」的工程直觉上。配合 fatal 时的自动堆栈(实测 CHECK 死亡现场自带地址栈),「多打点不罚、按需开灯」的调试体验完整成型------这套体验是后来者 spdlog 也要用 per-logger level 补课的原因。
5. Why:三个追问
① 流式语法为什么是那一代的选择? 因为 2010 年前后可变参数模板尚未普及(C++11 才落地),「类型安全地拼任意内容」的答案只有 operator<<;C 风格 printf 类型不安全、fmt 式 {} 占位要到 2015+ 才有库、2020 才进标准。glog 的 << 是那个年代的类型安全上限------今天看它冗长,是语言演进史叠在 API 上的地层(与第 4 篇 fmt 摘要里「手动挡」的对照:不是设计者不想优雅,是当时没有这套语法)。
② VLOG 的号码比级别好在哪? 好在多维度 :级别回答「这条消息多严重」,号码回答「这个模块想多说几句」------两个正交的旋钮不该挤在同一根轴上。--vmodule=sync=3,db=1(按模块设号)把细控推到模块粒度。spdlog 后来也吸收了这个思想(per-logger level),但「号码 + 模块名」的组合仍是 glog 最锋利的地方。
③ CHECK 为什么适合生产? 三个设计决定:NDEBUG 不影响 (与 assert 的本质区别------不变量不该随编译配置蒸发);失败即退出 (坏不变量下继续跑是更大的事故,与第 17 篇 gflags 校验器同哲学:错误离定义点越近越好);先报告再死(F 级日志 + 堆栈,尸体自带遗书)。测试代码里它配合 gtest 的死亡断言食用更佳(〔关联 第 21 篇〕)。
文件模式的默认行为是「写到临时文件、达到大小切档、保留 N 个」------新手常见「明明没 logtostderr 却哪都没输出」其实是写进了 *.log.INFO. 系列文件(当前目录或 log_dir)------先找文件再怀疑配置 。 severity 链也值得知道:ERROR 级默认同时写 INFO/WARNING/ERROR 三个文件(高层级文件是低层级的超集)------排查时看最专门的那个文件。
「先初始化后使用」的时间窗也有讲究:全局对象在 InitGoogleLogging 之前打日志的行为未定义(常见是丢或崩)------让含 LOG 的全局对象滚出全局作用域是 glog 工程的一号规范;实在需要早期日志,把 init 提到 main 第一行(本篇实测姿势)或用局部静态延迟。
6. 坑与最佳实践(实测依据)
- 必须
InitGoogleLogging先行,且 argv0 会成为日志文件名前缀------改名从源头改。 - 默认写文件不打屏 (与 spdlog 相反):开发期先
FLAGS_logtostderr = 1,否则新手会以为「日志没生效」。 - 手工链静态库要过导出宏 :直连
.a报not included correctly(实测);用 CMake target 一了百了。 - ERROR 级别在部分版本有额外动作(如刷栈、计数),热路径慎用------它是给「要出事了」的,不是「值得注意」。
- 与 gflags 是天作之合 :
FLAGS_logtostderr、FLAGS_v本身就是 gflags 标志------第 17 篇的全局标志模型在这里发光:命令行--v=2即时调音量。
共存叙事收尾:两者常在同一个进程里出现(新模块 spdlog、存量 glog)------文件各写各的没有冲突,但级别词汇要先对齐(glog 的 ERROR 对应 spdlog 的 err 还是 warn,团队要有一张对照表),否则跨文件的日志检索会被级别落差撕裂。迁移的第一步不是换库,是统一词汇。
(决策再压缩一句:新代码 spdlog、存量与 CHECK 重度 glog、两者共存先统一级别词汇表。)
7. 选型对比:与 spdlog 的世代差
| glog | spdlog | |
|---|---|---|
| 语法 | LOG(X) << 流式 |
info("{}", x) fmt 式 |
| 细控 | VLOG 号码 + 模块名 | per-logger 级别 |
| 落盘 | 内建策略(偏服务端习惯) | sink 组合更灵活 |
| 断言 | CHECK 家族 | ❌(非日志职责) |
| 定位 | Google 系/存量/要 CHECK | 新项目默认 |
新项目选 spdlog、要 CHECK 自己加 assert 库;存量与 Google 生态选 glog------两个都懂,迁移时才不慌。
一个冷知识收尾:glog 的 LogMessage 死亡堆栈(实测 CHECK 失败时那串地址)在符号化后能精确定位到断言行------生产环境里「CHECK 崩了但没人知道哪行」的迷案,解法就是拿地址对符号文件离线解析(第 20 篇的技能)。glog 与 backward-cpp 在「死也要留下遗言」这件事上是同一条战壕。
8. 延伸与联动
- 官方 github.com/google/glog------README 的 flag 清单是运维调参速查表;
- 第 17 篇 gflags(全局标志模型)、本篇(上一代标准)与下一篇 spdlog(现代答案)构成日志三视角;
- 下一篇 backward-cpp:日志管「活着的时候」,崩溃后的现场交给谁。〔关联 第 20 篇〕
参考 :google/glog 0.7.1(BSD-3)。文中 I/W/E/VLOG 开关、CHECK 失败报文与非零退出均为本机实测(g++ 13.1,CMake target 接入)。