Go 部落格
使用 slog 的結構化記錄
Go 1.21 中新的 log/slog
套件為標準函式庫加入結構化記錄功能。結構化記錄使用關鍵字值對,因此可以快速且可靠地解析、篩選、搜尋和分析。對於伺服器而言,記錄是開發人員用來觀察系統詳細資料行為的一種重要方式,而且通常是他們進行除錯的第一個地方。因此,記錄通常內容繁多,因此具備快速搜尋和篩選功能至關重要。
標準函式庫自從 Go 在十多年前首次發行以來,就已經具備記錄套件 log
。隨著時間推移,我們得知結構化記錄對 Go 程式設計師而言非常重要。在我們的年度調查中,結構化記錄一直名列前茅,而且 Go 生態系中的許多套件也提供這種功能。其中有些相當熱門:最早的 Go 結構化記錄套件之一 logrus,在超過 10 萬個其他套件中使用。
在有許多結構化記錄套件可供選擇的情況下,大型程式通常會透過其依賴關係納入一個以上的套件。主程式可能必須設定這些記錄套件的每項設定,以便記錄輸出是一致的:全部存放在同一個位置,且採用相同的格式。透過將結構化記錄納入標準函式庫中,我們可以提供一個所有其他結構化記錄套件都能共用的共通架構。
slog
的介紹
下面是使用 slog
做示範的最簡單程式
package main
import "log/slog"
func main() {
slog.Info("hello, world")
}
截至撰寫本文為止,該程式會列印
2023/08/04 16:09:19 INFO hello, world
Info
函式使用預設記錄器在資訊記錄層級列印訊息,在這種情況下,預設記錄器來自 log
套件 — 當你撰寫 log.Printf
時會取得相同的記錄器。這說明了為什麼輸出的外觀如此相似:只有「資訊」是新的。slog
和原本的 log
套件開箱即用,可以輕鬆地開始使用。
除了 Info
之外,還有三種其他層級的函式 — Debug
、Warn
和 Error
— 以及將層級當成引數傳遞的更一般化 Log
函式。在 slog
中,層級只是整數,因此你不會僅限於這四個命名層級。例如,Info
是 0,Warn
是 4,因此如果你的記錄系統在這些層級之間有一個層級,你可以為它使用 2。
與 log
套件不同的是,我們可以輕易在訊息後撰寫鍵值對,將該對新增至我們的輸出
slog.Info("hello, world", "user", os.Getenv("USER"))
輸出現在看起來像這樣
2023/08/04 16:27:19 INFO hello, world user=jba
如我們所述,slog
的頂層函式使用預設記錄器。我們可以明確取得這個記錄器,並呼叫它的各種方法
logger := slog.Default()
logger.Info("hello, world", "user", os.Getenv("USER"))
每個頂層函式都對應於 slog.Logger
上的方法。輸出與之前相同。
最初,slog 的輸出會透過預設的 log.Logger
,產生如上所述的輸出。我們可以透過變更記錄器所使用的處理常式來變更輸出。slog
內建兩個處理常式。TextHandler
會以 key=value
的形式發射所有記錄資訊。這個程式會使用 TextHandler
建立一個新的記錄器,並呼叫 Info
方法
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("hello, world", "user", os.Getenv("USER"))
現在輸出看起來像這樣
time=2023-08-04T16:56:03.786-04:00 level=INFO msg="hello, world" user=jba
一切都已轉換成一組鍵值對,且視需要將字串加上引號以保留結構。
如需 JSON 輸出,請改為安裝內建的 JSONHandler
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("hello, world", "user", os.Getenv("USER"))
現在我們的輸出是一連串的 JSON 物件,每個呼叫一個記錄
{"time":"2023-08-04T16:58:02.939245411-04:00","level":"INFO","msg":"hello, world","user":"jba"}
你並不受限於內建的處理函式。任何人都可以透過實作 `slog.Handler` 介面來編寫處理函式。處理函式可以產生特定格式的輸出,或包裝另一個處理函式以新增功能。`slog` 說明文件中的其中一個範例展示了如何編寫一個包裝處理函式,它會變更顯示記錄訊息所需的最低層級。
屬性所使用的交替金鑰/值語法迄今仍很方便,但對於執行頻繁的記錄陳述而言,使用 `Attr` 型態並呼叫 `LogAttrs` 方法可能更有效率。它們會協同運作以將記憶體配置降至最低。有些函式可以用字串、數字和其它常見型態來建構 `Attr`。這個對 `LogAttrs` 的呼叫會產生與上面相同的輸出,但執行速度較快
slog.LogAttrs(context.Background(), slog.LevelInfo, "hello, world",
slog.String("user", os.Getenv("USER")))
還有許多關於 `slog` 的議題
-
正如對 `LogAttrs` 的呼叫所顯示的, możesz 傳送 `context.Context` 給一些記錄函式,讓處理函式可以擷取追蹤 ID 等脈絡資訊。(取消脈絡並不會阻止記錄條目的編寫。)
-
你可以呼叫 `Logger.With` 以將屬性新增到將會出現在其所有輸出的記錄器,有效提取多個記錄陳述的公用部分。這不僅方便,還能提升效能,如下所述。
-
屬性可以合併成群組。這可以為你的記錄輸出新增更多結構,並有助於避免混淆,而這些混淆可能會讓金鑰看起來一致。
-
你可以透過提供方法 `LogValue` 並包含型態,來控制值在記錄中出現的方式。這可以用於將結構的欄位記錄成一個群組或《a href="https://pkg.go.dev/log/slog@master#example-LogValuer-Secret" rel="noreferrer" target="_blank">編輯敏感資料等更多作業。
深入了解所有 `slog` 的最棒去處是套件說明文件。
效能
我們希望 `slog` 能夠執行得很快。為了顯著提升大規模效能,我們設計`Handler` 介面,以提供最佳化機會。`Enabled` 方法會在每個記錄事件開始時呼叫,讓處理函式有機會快速刪除不需要的記錄事件。`WithAttrs` 和 `WithGroup` 方法讓處理函式能一次性格式化 `Logger.With` 所新增的屬性,而不是在每次記錄呼叫時格式化。這種預先格式化可以在將大量的屬性(如 `http.Request`)新增到 `Logger`,並在許多記錄呼叫中使用時,大幅提升速度。
為了通知我們的效能最佳化工作,我們調查了現有開源專案中記錄的典型模式。我們發現超過 95% 對記錄方法的呼叫傳遞了五個或更少的屬性。我們也對屬性的類型進行分類,發現少數常見類型占了大多數。我們接著撰寫捕捉常見案例的基準,並將其用作指南以了解花費時間的地方。最大的收穫來自仔細注意記憶體配置。
設計過程
自從 2012 年 Go 1 發布以來,slog
套件是標準程式庫中最大的新增功能之一。我們想花時間設計它,而且我們知道社群回饋會很重要。
到 2022 年 4 月,我們已經蒐集了足夠的資料來證明結構化記錄對 Go 社群的重要性。Go 團隊決定探索將其新增至標準程式庫。
我們從查看現有的結構化記錄套件如何設計開始。我們也利用儲存在 Go 模組代理程式中的大量開源 Go 程式碼集合來了解這些套件如何實際上被使用。我們的第一次設計是由這項研究以及 Go 的簡潔精神所啟發。我們想要一個頁面輕巧且容易了解的 API,同時不犧牲效能。
我們從未有過取代現有第三方記錄套件的目標。他們都擅長他們的工作,而且更換運作良好的現有程式碼很少能善用開發人員的時間。我們將 API 分成前端 Logger
,它會呼叫後端介面 Handler
。這樣一來,現有的記錄套件就能與共同後端溝通,所以使用它們的套件便能夠互通,而不需要重新撰寫。已撰寫或正在撰寫處理常規記錄套件的大量處理器,包括 Zap、logr 和 hclog。
我們在 Go 團隊內與其他擁有豐富記錄經驗的開發人員分享了我們最初的設計。我們根據他們的回饋進行修改,並且在 2022 年 8 月,我們認為我們有一個可行的設計。在 8 月 29 日,我們公開了我們的 實驗性實作,並開始了一個 GitHub 討論 來聽聽社群的看法。回應熱烈且主要都是正面的。感謝其他結構化記錄套件的設計者和使用者的見解深刻的評論,我們進行了數次變更,並新增了某幾個功能,例如群組和 LogValuer
介面。我們兩次變更了從記錄層級到整數的對應。
在兩個月和約 300 則意見後,我們感覺已經為實際的 提案 和隨附的 設計文件 做好準備。提案議題收集了超過 800 則意見,並促成 API 及實作的許多改進。以下是兩個 API 變更範例,都與 context.Context
有關
-
原本 API 支援將記錄器新增到 context。許多人認為這是一種便利的方式,可以在不注意記錄器的程式碼層級間輕易地串接記錄器。但其他人則認為這會夾帶不明確的相依性,讓程式碼更難懂。最後,我們移除這項功能,因為它太具爭議性。
-
我們也針對傳遞 context 至記錄方法的相關問題進行了激烈的爭論,試驗了許多設計。我們一開始 сопротивление standard 慣例(將 context 傳遞為第一個引數),因為我們不希望每個記錄呼叫都需要 context,但最後還是建立了兩組記錄方法,一組帶有 context,另一組沒有。
我們沒有進行的一項變更,是關於用於表示屬性的交替鍵值語法
slog.Info("message", "k1", v1, "k2", v2)
許多人強烈認為這是一個壞主意。他們覺得它難以閱讀,而且容易遺漏鍵或值而寫錯。他們偏好使用明確的屬性來表達結構
slog.Info("message", slog.Int("k1", v1), slog.String("k2", v2))
但我們認為輕量語法對於讓 Go 保持易用且有趣至關重要,特別是對於新的 Go 程式設計師。我們也知道許多 Go 記錄套件,例如 logr
、go-kit/log
和 zap
(及其 SugaredLogger
),都成功地使用了交替鍵與值。我們新增了 vet 檢查 來找出常見錯誤,但沒有變更設計。
在 2023 年 3 月 15 日,提案獲得接受,但仍有一些微小的待解決議題。在接下來的幾星期內,提出了十項額外的變更,並予以解決。到了七月初,log/slog
套件實作已完成,以及 testing/slogtest
套件(用於驗證處理常式),和交替鍵值正確使用的 vet 檢查。
在 8 月 8 日,Go 1.21 發布,其中包含 slog
。我們希望您覺得它很好用,而且使用起來像建置時一樣有趣。
並且非常感謝參與討論和提案程式的每個人。您的貢獻讓 slog
大大改善。
資源
log/slog
套件的 文件 說明了如何使用它,並提供了許多範例。
此 wiki 頁面 具有 Go 社群提供的其他資源,包括各式各樣的處理常式。
如果您想撰寫一個處理常式,請參閱 處理常式撰寫指南。
下一篇文章:完美可重現,已驗證的 Go 工具鏈
上一篇文章:Go 1.21 中的前向相容性和工具鏈管理
網誌索引