☰
glog 自定义 Sink(Custom Sinks)完整指南:将日志输出到任意目的地
2026/9/28 13:01:42 网站建设 项目流程
  • 后端

【免费下载链接】glog

C++ implementation of the Google logging module

项目地址:https://gitcode.com/gh_mirrors/glog6/glog
点击查看免费下载

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"; }

逐段解读:

  1. 实现自定义 sink:MyLogSink继承google::LogSink并重写send(),把日志级别名、源文件名、行号和消息正文写到std::cout。这里用GetLogSeverityName(severity)得到级别字符串(如INFO),用std::copy_n按message_len精确拷贝消息正文。
  2. 注册 sink:调用google::AddLogSink(&sink)后,全局日志宏(如LOG(INFO))产生的日志就会同时出现在std::cout。
  3. 注销 sink:不再需要时调用google::RemoveLogSink(&sink)从注册表移除。注意 sink 是栈上对象,注销后其生命周期随作用域结束而自然结束,这正是文档强调的“先注销、后销毁”的正确范式。
  4. 直接定向:即使不注册,也可以用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 时有以下几点需要牢记:

  1. 不要在send()内使用LOG()/CHECK():send()执行期间日志系统持有互斥锁,嵌套日志会死锁(文档docs/sinks.md中的 warning,以及 src/glog/logging.h 均有说明)。若确需在 send 逻辑中记日志,可考虑队列 + 后台线程模式,并用WaitTillSent()同步。
  2. send()实现必须线程安全:注册的 sink 是共享实例,会被产生LOG(XXX)的任意线程调用(src/glog/logging.h 注释明确要求 implementations must be thread-safe)。多个线程同时写同一 sink 时,需要自行加锁或保证目标输出本身线程安全。
  3. 所有权与生命周期:LogSink实例在调用RemoveLogSink之前不能被销毁。最佳实践是让 sink 的生命周期覆盖整个注册区间(如栈对象 + 作用域管理,或std::unique_ptr长期持有)。
  4. 区分两种使用方式:全局注册(AddLogSink)影响所有日志宏;直接定向(LOG_TO_SINK系列宏)只影响单条消息且不必注册。两种方式可同时使用,注意注册后LOG_TO_SINK的消息会同时走全局 sink 和指定 sink。
  5. 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

项目地址:https://gitcode.com/gh_mirrors/glog6/glog
点击查看免费下载
上一篇:如何配置dnGrep实现批量文件搜索和替换:完整操作流程
下一篇:Chakram核心功能解析:HTTP状态码、Cookie与响应时间测试实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询