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

Kubernetes 1.24 中的上下文日志记录

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

结构化日志

结构化日志 的目标是用具有明确语法(用于单独存储消息和参数,例如作为 JSON 结构体)的日志条目替换 C 风格的格式化和由此产生的模糊日志字符串。

当对结构化日志调用使用传统的 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。核心思想是库通过调用者传递一个 logger 实例,并使用该实例进行日志记录,而不是访问全局 logger。日志实现的决定权在于二进制程序,而不是库。go-logr API 围绕结构化日志设计,并支持向 logger 附加额外信息。

这启用了额外的用例

  • 调用者可以将额外信息附加到 logger

    当将这个扩展的 logger 传递给一个函数,并且函数使用它而不是全局 logger 时,额外的信息就会包含在所有日志条目中,而无需修改生成日志条目的代码。这在高度并行的应用程序中非常有用,因为不同操作的输出会交织在一起,很难识别某个特定操作的所有日志条目。

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

上下文日志的设计决策之一是允许将 logger 作为值附加到 context.Context。由于 logger 封装了调用所需的所有日志记录方面,它属于上下文的一部分,而不仅仅是使用上下文。一个实际的好处是许多 API 已经有 ctx 参数,或者添加一个可以带来额外的好处,例如能够在函数内部移除 context.TODO() 调用。

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

  • 在设置了上下文日志记录的二进制程序中,使用传统 klog 日志记录调用的库仍然可以工作,并通过二进制程序选择的日志后端进行日志记录。但是,此类日志输出不会包含额外信息,并且在单元测试中效果不佳,因此应修改库以支持上下文日志记录。结构化日志迁移指南 已扩展,也涵盖了上下文日志记录。

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

在 Kubernetes 1.24 中,上下文日志记录是一项新的 Alpha 特性, feature gate 为 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

以下调用管理 logger 的查找

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

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

在 Kubernetes 代码中强制使用这些函数,并通过 linter 检查来执行。klog 对上下文日志记录的默认设置是启用该功能,因为它在 klog 中被认为是稳定的。仅在 Kubernetes 二进制程序中,该默认设置才会被覆盖,并且(在某些二进制程序中)通过 --feature-gate 参数进行控制。

ktesting logger

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

klogr

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

然而,不鼓励使用这种方式,因为该格式既不是机器可读的(与 Kubernetes 使用的 go-logr 实现 zapr 生成的真实 JSON 输出不同),也不是人类友好的(与 klog 文本格式不同)。

相反,应该使用 WithFormat(FormatKlog) 创建 klogr 实例,这将选择 klog 文本格式。一种获得相同结果的更简单构造方法是新的 klog.NewKlogr。这是当没有其他配置时,klog 作为回退返回的 logger。

可复用输出测试

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

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

输出刷新

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

当 go-logr 实现对数据进行缓冲时,可以通过使用 FlushLogger 选项注册 logger,将数据刷新集成到 klog.Flush 中。

其他各项变更

有关所有其他增强功能的说明,请参阅发布说明。

logcheck

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

它可以作为插件包含在 golangci-lint 调用中(这是Kubernetes 现在的使用方式),也可以独立调用。

我们正在将该工具迁移到一个新的仓库,因为它与 klog 并非真正相关,并且其版本应该被恰当地跟踪和标记。

后续步骤

结构化日志工作组 (Structured Logging WG) 一直在寻找新的贡献者。从 C 风格日志的迁移现在将一步到位地瞄准结构化、上下文相关的日志,以减少整体代码变动和拉取请求 (PR) 数量。修改日志调用是贡献给 Kubernetes 的一个很好的入门机会,也是了解不同领域代码的机会。