- 后端
【免费下载链接】glog
C++ implementation of the Google logging module
glog 默认将日志写入文件、stderr或stdout,但许多场景需要把日志送到文件之外的任意目的地。本文基于 glog 官方文档docs/sinks.md,深入讲解google::LogSink接口的完整用法:如何自定义实现send()、如何通过AddLogSink/RemoveLogSink注册与注销、如何用LOG_TO_SINK系列宏直接定向输出,并给出可编译运行的完整示例与底层源码级实现原理。
何时需要自定义 Sink
在默认配置下,glog 会把日志写到本地文件、stderr或stdout(具体行为由FLAGS_logtostderr、FLAGS_logtostdout等标志控制,参见 docs/flags.md)。但在某些场景下,这些默认目的地并不够用,例如:
- 将日志发送到网络日志收集服务(如 syslog、Kafka、远程日志聚合平台);
- 将日志写入数据库或其他持久化存储;
- 将日志转发到消息队列供下游消费;
- 在 GUI 应用中把日志显示到界面;
- 对日志做二次加工后再输出(例如脱敏、格式化、统计)。
此时 glog 提供了google::LogSink接口:实现该接口后,库会在每次记录日志时调用你的实现,把日志内容写到任意你想要的地方。
LogSink 基本接口
LogSink接口定义在 src/glog/logging.h,核心是一个纯虚函数:
class LogSink { public: virtual ~LogSink(); // 每次记录日志时被库调用。 // message_len 不包含末尾的 '\n'。 // 注意:本方法内不能使用 LOG() 或 CHECK()! virtual void send(LogSeverity severity, const char* full_filename, const char* base_filename, int line, const LogMessageTime& time, const char* message, size_t message_len) = 0; // 可选:等待 send() 的日志逻辑全部完成,默认空实现。 virtual void WaitTillSent(); // 静态辅助函数:返回日志消息的标准文本输出(含时间戳前缀等)。 static std::string ToString(LogSeverity severity, const char* file, int line, const LogMessageTime& time, const char* message, size_t message_len); };你需要继承LogSink并实现send()。各参数含义如下:
| 参数 | 含义 |
|---|---|
severity | 日志级别,INFO/WARNING/ERROR/FATAL之一,对应枚举google::LogSeverity(定义见 src/glog/log_severity.h) |
full_filename | 触发日志的源文件完整路径 |
base_filename | 触发日志的源文件名(不含目录) |
line | 触发日志的行号 |
time | 日志时间,类型为LogMessageTime |
message | 日志消息正文(不含时间戳前缀,且不包含末尾换行符) |
message_len | 消息正文的字节长度 |
注意(嵌套日志导致死锁):
send()被调用时,日志系统持有内部互斥锁(mutex),因此在send()内严禁再调用LOG()或CHECK(),否则会造成死锁。这在头文件注释中也有明确说明(见 src/glog/logging.h)。
接口还提供了两个辅助成员:
WaitTillSent():可选重写。在每次send()返回之后、LogMessage对象退出(或崩溃)之前被调用,默认什么都不做。利用它可以实现更复杂的send()逻辑:例如在send()中把消息加入队列并唤醒另一个线程去真正写日志,同时自身可以安全地调用LOG();而WaitTillSent()则等待该线程处理完成,从而既避免死锁又保证消息顺序(源码注释见 src/glog/logging.h)。ToString(...):静态方法,把日志参数渲染成 glog 标准的文本行([IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg格式),实现见 src/logging.cc。在实现send()时可用它直接得到与文件日志一致的格式化文本。
注册与注销 Sink
实现好LogSink后,通过以下两个线程安全的函数注册或注销(声明见 src/glog/logging.h,实现见 src/logging.cc):
void google::AddLogSink(LogSink* destination); void google::RemoveLogSink(LogSink* destination);AddLogSink把 sink 指针加入全局注册表,此后所有通过LOG()、CHECK()等宏产生的日志都会同时发送给该 sink;RemoveLogSink把 sink 从注册表移除,之后不再接收日志;- 两者内部都用互斥锁保护(
SinkLock l{sink_mutex_}),见 src/logging.cc,因此线程安全,可在任意线程调用。
危险(所有权管理):注册表保存的是指针而非对象拷贝。在调用
RemoveLogSink解除注册之前,严禁销毁该LogSink实例,否则注册表将持有悬空指针,后续日志触发send()时会造成未定义行为。
从源码看,注册的 sink 会存放在一个std::vector<LogSink*>中(src/logging.cc);每次记录日志时,LogDestination::LogToSinks会持共享锁遍历该 vector 依次调用每个 sink 的send()(src/logging.cc)。RemoveLogSink用std::remove加erase删除匹配指针,不保证保留原有顺序(源码注释明确“doesn't keep the sinks in order”)。
直接定向日志(不注册也可用)
除了全局注册,还可以在不注册的情况下,把某条具体日志直接定向到某个 sink 实例。glog 提供了两个宏(定义见 src/glog/logging.h):
LOG_TO_SINK(sink, severity) LOG_TO_SINK_BUT_NOT_TO_LOGFILE(sink, severity)LOG_TO_SINK(sink, severity):把消息同时发送给该 sink并且照常写入全局日志(如日志文件),相当于“双写”;LOG_TO_SINK_BUT_NOT_TO_LOGFILE(sink, severity):只发送给该 sink,不写入日志文件等全局日志目的地。
两者的第二个参数severity必须是INFO、WARNING、ERROR、FATAL之一,不能是变量(因为宏内部拼成google::GLOG_##severity)。
底层原理(见 src/logging.cc):LOG_TO_SINK展开为LogMessage(..., sink, /*also_send_to_log=*/true),最终调用LogMessage::SendToSinkAndLog()——先SendToSink()再把消息交给常规日志链路(src/logging.cc);LOG_TO_SINK_BUT_NOT_TO_LOGFILE则传false,只调用SendToSink()(src/logging.cc)。SendToSink中可以看到send()收到的message是从message_text_中剔除前缀(num_prefix_chars_)和末尾换行后的正文,与接口文档描述一致。
完整示例:把日志输出到 std::cout
仓库中 examples/custom_sink.cc 给出了一个完整可编译运行的示例,展示了注册/注销与直接定向两种用法:
#include <glog/logging.h> #include <algorithm> #include <cstddef> #include <iostream> #include <iterator> namespace { struct MyLogSink : google::LogSink { // (1)! void send(google::LogSeverity severity, const char* /*full_filename*/, const char* base_filename, int line, const google::LogMessageTime& /*time*/, const char* message, std::size_t message_len) override { std::cout << google::GetLogSeverityName(severity) << ' ' << base_filename << ':' << line << ' '; std::copy_n(message, message_len, std::ostreambuf_iterator<char>{std::cout}); std::cout << '\n'; } }; } // namespace int main(int /*argc*/, char** argv) { google::InitGoogleLogging(argv[0]); MyLogSink sink; google::AddLogSink(&sink); // (2)! LOG(INFO) << "logging to MySink"; google::RemoveLogSink(&sink); // (3)! // 不注册也可以直接向 sink 写日志 LOG_TO_SINK(&sink, INFO) << "direct logging"; // (4)! LOG_TO_SINK_BUT_NOT_TO_LOGFILE(&sink, INFO) << "direct logging but not to file"; }逐段解读:
- 实现自定义 sink:
MyLogSink继承google::LogSink并重写send(),把日志级别名、源文件名、行号和消息正文写到std::cout。这里用GetLogSeverityName(severity)得到级别字符串(如INFO),用std::copy_n按message_len精确拷贝消息正文。 - 注册 sink:调用
google::AddLogSink(&sink)后,全局日志宏(如LOG(INFO))产生的日志就会同时出现在std::cout。 - 注销 sink:不再需要时调用
google::RemoveLogSink(&sink)从注册表移除。注意 sink 是栈上对象,注销后其生命周期随作用域结束而自然结束,这正是文档强调的“先注销、后销毁”的正确范式。 - 直接定向:即使不注册,也可以用
LOG_TO_SINK/LOG_TO_SINK_BUT_NOT_TO_LOGFILE把特定消息发给该 sink。
运行示例(把日志目录设为当前目录,使日志文件落在本地):
GLOG_log_dir=. ./custom_sink_example终端(stdout)输出:
INFO custom_sink.cc:63 logging to MySink INFO custom_sink.cc:68 direct logging INFO custom_sink.cc:69 direct logging but not to file对应的日志文件内容(custom_sink_example.INFO之类的文件):
Log file created at: 2024/06/11 13:24:27 Running on machine: pc Running duration (h:mm:ss): 0:00:00 Log line format: [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg I20240611 13:24:27.476620 126237946035776 custom_sink.cc:63] logging to MySink I20240611 13:24:27.476796 126237946035776 custom_sink.cc:68] direct logging注意对比:custom_sink.cc:69的direct logging but not to file只出现在 stdout 中,没有出现在日志文件里——这正是LOG_TO_SINK_BUT_NOT_TO_LOGFILE的效果(LOG_TO_SINK那条则会同时写入文件)。这是验证两个宏行为差异的直观手段。
实践要点与注意事项
结合文档警告与源码实现,使用自定义 sink 时有以下几点需要牢记:
- 不要在
send()内使用LOG()/CHECK():send()执行期间日志系统持有互斥锁,嵌套日志会死锁(文档docs/sinks.md中的 warning,以及 src/glog/logging.h 均有说明)。若确需在 send 逻辑中记日志,可考虑队列 + 后台线程模式,并用WaitTillSent()同步。 send()实现必须线程安全:注册的 sink 是共享实例,会被产生LOG(XXX)的任意线程调用(src/glog/logging.h 注释明确要求 implementations must be thread-safe)。多个线程同时写同一 sink 时,需要自行加锁或保证目标输出本身线程安全。- 所有权与生命周期:
LogSink实例在调用RemoveLogSink之前不能被销毁。最佳实践是让 sink 的生命周期覆盖整个注册区间(如栈对象 + 作用域管理,或std::unique_ptr长期持有)。 - 区分两种使用方式:全局注册(
AddLogSink)影响所有日志宏;直接定向(LOG_TO_SINK系列宏)只影响单条消息且不必注册。两种方式可同时使用,注意注册后LOG_TO_SINK的消息会同时走全局 sink 和指定 sink。 message_len语义:send()收到的message不含时间戳前缀和末尾换行,message_len即正文长度;如需完整标准格式,可用静态方法LogSink::ToString(...)生成。
相关源码与文档速查
- 接口定义与宏声明:src/glog/logging.h、src/glog/logging.h
- 注册/注销实现:src/logging.cc、src/logging.cc
- sink 分发与等待逻辑:src/logging.cc
- 直接定向的底层路径:src/logging.cc、src/logging.cc
- 完整可运行示例:examples/custom_sink.cc
- 日志级别枚举:src/glog/log_severity.h
- 日志标志与默认行为:docs/flags.md
- 日志总体使用指南:docs/logging.md
- 后端
【免费下载链接】glog
C++ implementation of the Google logging module
相关推荐
高效glog日志格式自定义完整指南:打造专业级日志输出
高效glog日志格式自定义完整指南:打造专业级日志输出 想要提升C++应用程序的日志输出质量吗?Google Logging Library glog 提供了强
后端Terragrunt 日志格式化完全指南:用 `--log-custom-format` 自定义日志输出
Terragrunt 日志格式化完全指南:用 log custom format 自定义日志输出 Terragrunt 作为 OpenTofu/Terrafor
CLIDevOps云原生Hydra 日志自定义指南:基于 dictConfig 定制 Hydra 与任务日志输出
Hydra 日志自定义指南:基于 dictConfig 定制 Hydra 与任务日志输出 Hydra 在内部直接使用 Python 标准库 logging 的
开发工具后端CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考