消息记录

消息记录

g_return 系列宏(g_return_if_fail()g_return_val_if_fail()g_return_if_reached()g_return_val_if_reached())仅应用于编程错误,一个典型用例是在公共函数开头时检查无效参数。如果只是“if (error) return”,则不应使用它们,只有在“if (bug in program) return”情况下才应使用。在其中一项检查失败后,通常将程序行为视为未定义。它们不适用于正常控制流,而只在放弃前提供一个可能有用的警告。

使用 g_log_structured() 支持结构化记录输出。与传统的 g_log() API 不同,该处是将日志消息视为表示各个信息片段的关键值对集合,而非作为包含任意格式的所有信息的单个字符串来处理的。

方便的宏 g_info()g_message()g_debug()g_warning()g_error() 将使用传统的 g_log() API,除非在包含 glib.h 之前定义了符号 G_LOG_USE_STRUCTURED。但请注意,即使通过传统的 g_log() API 记录的消息最终也会传递到 g_log_structured(),这样所有日志消息最终都会进入同一目的地。如果定义了 G_LOG_USE_STRUCTURED,则 g_test_expect_message() 将对包装器宏 g_warning() 及类似项无效(请参见测试消息)。

以下需求推动了对结构化记录的支持(其中一些以前得到了支持,另一些则尚未得到支持):

  • 支持多个记录级别。
  • 通过添加 MESSAGE_ID 的功能提供结构化日志支持(请参见 g_log_structured())。
  • 将过滤日志消息的职责从程序转移到日志查看器——而不是让库和程序安装在输出之前过滤消息的日志处理程序(通过 g_log_set_handler()),而是输出所有日志消息,并且日志查看器程序(例如 journalctl)必须对它们进行筛选。这基于这样的想法,即错误有时难以重现,因此最好记录所有可能的信息,然后使用工具来分析日志,而不是无法重现错误来获取其他日志数据。在对性能至关重要的部分中使用记录的代码应在发行版本中编译出 g_log_structured() 调用,而在调试版本中将其编译出来。
  • 一个单一的写入器函数,处理进程中所有库和程序代码中的所有日志消息;而不是多个日志处理程序,它们之间交互关系定义不充分。这允许程序通过更改写入器函数(例如记录到附加位置或更改使用哪些记录输出后备)轻松地更改其记录策略。GLib 提供的日志写入器函数公开,以使它们可用于程序的日志写入器。这允许单独保留日志写入器策略和实现。
  • 如果某个库想要向其所有日志消息(例如库状态)添加标准信息或对私有数据(例如密码或网络凭证)进行编辑,则它应在其 g_log_structured() 调用周围使用包装器函数,或在单个日志写入器函数中实现此功能。
  • 如果某个程序希望将上下文数据从 g_log_structured() 调用传递到其日志写入器函数,以便(例如)它可以使用正确的服务器连接来提交日志,则该用户数据可以用作零长度的 GLogField 传递给 g_log_structured_array()
  • 需要在终端上支持彩色输出,以使阅读日志变得更容易。

使用结构化记录

要使用结构化日志记录(而非旧式日志记录),请使用 g_log_structured()g_log_structured_array() 函数;或在包含任何 GLib 标头之前定义 G_LOG_USE_STRUCTURED,并使用 g_message()g_debug()g_error()(等)宏。

您无需定义 G_LOG_USE_STRUCTURED 来使用 g_log_structured(),但建议避免混淆。

日志域

日志域可以用来对日志消息的来源进行大体划分。通常,每个应用程序或库都有一个或几个日志域。G_LOG_DOMAIN 应用来定义当前编译单元的默认日志域 — 它通常定义于源文件的顶部或一组源文件的预处理器标记中。

日志域必须唯一,并且建议以应用程序或库的名称为日志域,后跟连字符和子域名(可选)。例如,bloatpadbloatpad-io

调试消息输出

默认日志函数(旧式 APIg_log_default_handler() 和结构化 APIg_log_writer_default())在默认情况下都忽略调试和信息消息,除非在 G_MESSAGES_DEBUG 环境变量中列出了这些消息的日志域(或将其设置为 all),或者在环境中设置了 DEBUG_INVOCATION=1

建议自定义日志写入器函数重新使用 G_MESSAGES_DEBUGDEBUG_INVOCATION 环境变量,而不是发明一个自定义环境变量,以便开发人员可以在各个项目中重新使用相同的调试技术和工具。自 GLib 2.68 起,这可以通过忽略 g_log_writer_default_would_drop() 返回 TRUE 的消息来实现。

检验消息

对于旧 g_log() API,在简单情况下可以使用 g_test_expect_message()g_test_assert_expected_messages() 来检查待测代码是否发出了给定的日志消息。对于结构化日志记录 API,这些函数已废弃,原因有几个

  • 它们依赖于一个内部队列,而对于很多用例来说,该队列过于死板,消息可能以多种顺序发送,某些消息可能以非确定性方式发送,或者消息可能由无关的日志域发送。
  • 它们不支持结构化日志字段。
  • 检查代码的日志输出是一种不好的测试方法,虽然对于使用 g_log() 的旧式代码来说这样做可能很有必要,但对于使用 g_log_structured() 的新代码来说应该避免这样做。

如果 g_log() 正在使用(且未定义 G_LOG_USE_STRUCTURED),它们仍将像以前一样工作。如果与结构化日志记录 API 一起使用,它们将不会执行任何操作。

不鼓励检查代码的日志输出:库在定义的行为期间不应该输出到 stderr,因此不应该对此进行测试。如果需要测试库在未定义行为期间的日志输出,则应将其限制为断言库在中止之前中止并打印适当的错误消息。这应该通过 g_test_trap_assert_stderr() 来完成。

如果确实有必要测试特定代码段发出的结构化日志消息,而代码无法重新构建为更适合于更传统的单元测试,你应该编写一个自定义日志写入器函数(参见 g_log_set_writer_func()),它会将所有日志消息追加到一个队列中。当你想要检查日志消息时,检查并清除队列,忽略不相关的日志消息(例如,来自测试对象以外的其他日志域)。