消息记录
消息记录
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
应用来定义当前编译单元的默认日志域 — 它通常定义于源文件的顶部或一组源文件的预处理器标记中。
日志域必须唯一,并且建议以应用程序或库的名称为日志域,后跟连字符和子域名(可选)。例如,bloatpad
或 bloatpad-io
。
调试消息输出
默认日志函数(旧式 API 的 g_log_default_handler()
和结构化 API 的 g_log_writer_default()
)在默认情况下都忽略调试和信息消息,除非在 G_MESSAGES_DEBUG
环境变量中列出了这些消息的日志域(或将其设置为 all
),或者在环境中设置了 DEBUG_INVOCATION=1
。
建议自定义日志写入器函数重新使用 G_MESSAGES_DEBUG
和 DEBUG_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()
),它会将所有日志消息追加到一个队列中。当你想要检查日志消息时,检查并清除队列,忽略不相关的日志消息(例如,来自测试对象以外的其他日志域)。