本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

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。其核心思想是,庫從其呼叫者那裡接收一個 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 特性,其特性門控為 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
背景
全域性回退,無意支援上下文日誌
TODO
全域性回退,但僅作為臨時解決方案,直到函式擴充套件為透過其引數接受 logger
SetLoggerWithOptions
更改回退 logger;當使用 ContextualLogger(true) 呼叫時,logger 可以直接被呼叫,此時日誌記錄將不經過 klog

為了支援 Kubernetes 中的特性門控機制,klog 為相應的 go-logr 呼叫提供了包裝器呼叫,並有一個全域性布林值控制它們的行為

在 Kubernetes 程式碼中使用這些函式是透過一個 linter 檢查強制執行的。klog 對於上下文日誌的預設設定是啟用該功能,因為它在 klog 中被認為是穩定的。只有在 Kubernetes 二進位制檔案中,該預設值才會被覆蓋,並且(在某些二進位制檔案中)透過 --feature-gate 引數進行控制。

ktesting logger

新的 ktesting 包實現了透過 testing.T 使用 klog 的文字輸出格式進行日誌記錄。它有一個用於配置測試用例的單一 API 呼叫,並支援命令列標誌

klogr

klog/klogr 繼續被支援,其預設行為不變:它使用自己的自定義格式格式化結構化日誌條目,並透過 klog 列印結果。

然而,不鼓勵這種用法,因為該格式既不是機器可讀的(與 zapr(Kubernetes 使用的 go-logr 實現)產生的真正 JSON 輸出相反),也不是人類友好的(與 klog 文字格式相反)。

相反,應該使用 WithFormat(FormatKlog) 建立 klogr 例項,它會選擇 klog 文字格式。一個新的 klog.NewKlogr 是一個更簡單的構造方法,結果相同。這是 klog 在沒有其他配置時作為回退返回的 logger。

可重用的輸出測試

許多 go-logr 實現都有非常相似的單元測試,它們檢查某些日誌呼叫的結果。如果開發人員不知道某些注意事項,例如一個在呼叫時會恐慌的 String 函式,那麼很可能處理這些注意事項的程式碼和單元測試都缺失了。

klog.test 是一組可重用的測試用例,可以應用於 go-logr 實現。

輸出重新整理

klog 過去在 init 期間無條件地啟動一個 goroutine,該 goroutine 以硬編碼的間隔重新整理緩衝資料。現在,該 goroutine 僅在需要時(即寫入帶緩衝的檔案時)啟動,並且可以透過 StopFlushDaemonStartFlushDaemon 進行控制。

當 go-logr 實現緩衝資料時,可以透過使用 FlushLogger 選項註冊 logger,將重新整理該資料整合到 klog.Flush 中。

其他各種變化

有關所有其他增強功能的描述,請參見釋出說明

logcheck

logcheck 工具最初設計為結構化日誌呼叫的 linter,現已增強以支援上下文日誌和傳統的 klog 日誌呼叫。這些增強的檢查已經在 Kubernetes 中發現了錯誤,例如使用格式化字串和引數呼叫 klog.Info 而不是 klog.Infof

它可以作為外掛包含在 golangci-lint 呼叫中,Kubernetes 現在就是這樣使用它的,也可以獨立呼叫。

我們正在將該工具遷移到一個新的倉庫,因為它與 klog 並無真正關聯,其釋出應該得到正確的跟蹤和標記。

後續步驟

結構化日誌工作組一直在尋找新的貢獻者。現在,從 C 風格日誌記錄的遷移將一步到位地轉向結構化的、上下文相關的日誌記錄,以減少整體的程式碼變動和 PR 數量。更改日誌呼叫是對 Kubernetes 的一個很好的首次貢獻,也是一個瞭解不同領域程式碼的機會。