本文发布时间已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。

Kubernetes 1.24 中的上下文日志记录

结构化日志工作组为 Kubernetes 1.24 中的日志基础设施添加了新功能。这篇博文解释了开发人员如何利用这些功能来使日志输出更有用,以及他们如何参与改进 Kubernetes。

结构化日志记录

结构化日志记录的目标是替换 C 风格的格式化和由此产生的不透明日志字符串,并使用具有明确定义的语法来单独存储消息和参数的日志条目,例如作为 JSON 结构。

当为结构化日志调用使用传统的 klog 文本输出格式时,字符串最初以 \n 转义序列打印,除非嵌入在结构内部。对于结构,日志条目仍然可以跨越多行,没有干净的方法将日志流拆分为单个条目

I1112 14:06:35.783529  328441 structured_logging.go:51] "using InfoS" longData={Name:long Data:Multiple
lines
with quite a bit
of text. internal:0}
I1112 14:06:35.783549  328441 structured_logging.go:52] "using InfoS with\nthe message across multiple lines" int=1 stringData="long: Multiple\nlines\nwith quite a bit\nof text." str="another value"

现在,<> 标记以及缩进用于确保在行首的 klog 标头处进行拆分是可靠的,并且生成的输出是人类可读的

I1126 10:31:50.378204  121736 structured_logging.go:59] "using InfoS" longData=<
	{Name:long Data:Multiple
	lines
	with quite a bit
	of text. internal:0}
 >
I1126 10:31:50.378228  121736 structured_logging.go:60] "using InfoS with\nthe message across multiple lines" int=1 stringData=<
	long: Multiple
	lines
	with quite a bit
	of text.
 > str="another value"

请注意,日志消息本身使用引号打印。它应该是一个固定的字符串,用于标识日志条目,因此应避免在此处使用换行符。

在 Kubernetes 1.24 之前,kube-scheduler 中的一些日志调用仍然使用 klog.Info 来处理多行字符串,以避免不可读的输出。现在,所有日志调用都已更新为支持结构化日志记录。

上下文日志记录

上下文日志记录基于 go-logr API。关键思想是,库由其调用者传递一个日志记录器实例,并使用该实例进行日志记录,而不是访问全局日志记录器。二进制文件决定日志记录实现,而不是库。go-logr API 是围绕结构化日志记录设计的,并支持将其他信息附加到日志记录器。

这使得其他用例成为可能

  • 调用者可以将其他信息附加到日志记录器

    当将此扩展的日志记录器传递给函数,并且函数使用它而不是全局日志记录器时,其他信息将包含在所有日志条目中,而无需修改生成日志条目的代码。这在高度并行的应用程序中非常有用,在高度并行的应用程序中,由于来自不同操作的输出交错在一起,因此很难识别某个操作的所有日志条目。

  • 运行单元测试时,日志输出可以与当前测试相关联。然后,当测试失败时,只有失败测试的日志输出才会由 go test 显示。默认情况下,该输出也可以更详细,因为它不会显示给成功的测试。测试可以并行运行,而不会交错其输出。

上下文日志记录的设计决策之一是将日志记录器作为值附加到 context.Context。由于日志记录器封装了调用预期日志记录的所有方面,因此它是上下文的一部分,而不仅仅是使用它。一个实际的优势是,许多 API 已经有一个 ctx 参数,或者添加一个参数具有其他优势,例如能够摆脱函数内部的 context.TODO() 调用。

另一个决定是不破坏与 klog v2 的兼容性

  • 在已设置上下文日志记录的二进制文件中使用传统 klog 日志调用的库将通过二进制文件选择的日志记录后端工作和记录。但是,此类日志输出将不包含其他信息,并且在单元测试中无法正常工作,因此应修改库以支持上下文日志记录。结构化日志记录的迁移指南 已扩展到涵盖上下文日志记录。

  • 当库支持上下文日志记录并从其上下文中检索日志记录器时,它仍然可以在不初始化上下文日志记录的二进制文件中工作,因为它将获得一个通过 klog 记录的日志记录器。

在 Kubernetes 1.24 中,上下文日志记录是一个新的 alpha 功能,具有 ContextualLogging 作为功能门。禁用时(默认设置),上下文日志记录的新 klog API 调用(见下文)将变为无操作,以避免性能或功能上的倒退。

尚未转换任何 Kubernetes 组件。Kubernetes 存储库中的一个 示例程序 演示了如何在二进制文件中启用上下文日志记录以及输出如何取决于二进制文件的参数

$ cd $GOPATH/src/k8s.io/kubernetes/staging/src/k8s.io/component-base/logs/example/cmd/
$ go run . --help
...
      --feature-gates mapStringBool  A set of key=value pairs that describe feature gates for alpha/experimental features. Options are:
                                     AllAlpha=true|false (ALPHA - default=false)
                                     AllBeta=true|false (BETA - default=false)
                                     ContextualLogging=true|false (ALPHA - default=false)
$ go run . --feature-gates ContextualLogging=true
...
I0404 18:00:02.916429  451895 logger.go:94] "example/myname: runtime" foo="bar" duration="1m0s"
I0404 18:00:02.916447  451895 logger.go:95] "example: another runtime" foo="bar" duration="1m0s"

example 前缀和 foo="bar" 是由记录 runtime 消息和 duration="1m0s" 值的函数的调用者添加的。

klog 的示例代码包括一个 示例,用于使用每个测试输出进行单元测试。

klog 增强功能

上下文日志记录 API

以下调用管理日志记录器的查找

FromContext
来自 context 参数,回退到全局日志记录器
Background
全局回退,无意支持上下文日志记录
TODO
全局回退,但仅作为临时解决方案,直到函数扩展为通过其参数接受日志记录器
SetLoggerWithOptions
更改回退日志记录器;当使用 ContextualLogger(true) 调用时,日志记录器已准备好被直接调用,在这种情况下,将直接完成日志记录,而无需通过 klog

为了支持 Kubernetes 中的功能门机制,klog 为相应的 go-logr 调用提供了包装器调用和一个控制其行为的全局布尔值

使用那些 Kubernetes 代码中的功能是通过一个 linter 检查来强制执行的。klog 上下文日志记录的默认值是启用该功能,因为它在 klog 中被认为是稳定的。只有在 Kubernetes 二进制文件中,该默认值才会被覆盖,并且(在某些二进制文件中)通过 --feature-gate 参数进行控制。

ktesting 日志记录器

新的 ktesting 包通过使用 klog 的文本输出格式的 testing.T 实现日志记录。它有一个用于检测测试用例的 单个 API 调用 和对 命令行标志的支持

klogr

klog/klogr 继续受到支持,其默认行为保持不变:它使用自己的自定义格式格式化结构化日志条目,并通过 klog 打印结果。

但是,不建议使用此方法,因为该格式既不是机器可读的(与 zapr(Kubernetes 使用的 go-logr 实现)生成的实际 JSON 输出相反),也不是对用户友好的(与 klog 文本格式相反)。

相反,应该使用 WithFormat(FormatKlog) 创建 klogr 实例,该实例选择 klog 文本格式。具有相同结果的更简单的构造方法是新的 klog.NewKlogr。这是 klog 在未配置任何其他内容时作为回退返回的日志记录器。

可重用的输出测试

许多 go-logr 实现都有非常相似的单元测试,它们在其中检查某些日志调用的结果。如果开发人员不知道某些注意事项,例如,当调用时会发生恐慌的 String 函数,那么很可能缺少对这些注意事项的处理和单元测试。

klog.test 是一组可重用的测试用例,可以应用于 go-logr 实现。

输出刷新

klog 过去在 init 期间无条件地启动一个 goroutine,该 goroutine 以硬编码的时间间隔刷新缓冲的数据。现在,该 goroutine 仅按需启动(即当写入带缓冲的文件时),并且可以使用 StopFlushDaemonStartFlushDaemon 进行控制。

当 go-logr 实现缓冲数据时,可以通过向 FlushLogger 选项注册日志记录器,将刷新数据集成到 klog.Flush 中。

其他各种更改

有关所有其他增强功能的描述,请参阅发行说明

logcheck

logcheck 工具最初被设计为结构化日志调用的 linter,现在已得到增强,可以支持上下文日志和传统的 klog 日志调用。这些增强的检查已经在 Kubernetes 中发现了一些错误,例如使用格式字符串和参数调用 klog.Info 而不是 klog.Infof

它可以作为插件包含在 golangci-lint 调用中,Kubernetes 现在就是这样使用的,或者可以单独调用。

我们正在将该工具移动到一个新的存储库中,因为它实际上与 klog 无关,并且其发布应该被正确地跟踪和标记。

下一步

结构化日志工作组 (Structured Logging WG) 一直在寻找新的贡献者。现在,从 C 风格日志迁移的目标将一步到位,转向结构化、上下文日志,以减少整体代码改动和 PR 数量。更改日志调用是向 Kubernetes 做出贡献的好开端,也是了解各个不同领域代码的机会。