Go 官方部落格

使用 slog 進行結構化日誌記錄

Jonathan Amsterdam
2023 年 8 月 22 日

Go 1.21 中新的 log/slog 包將結構化日誌記錄帶入了標準庫。結構化日誌使用鍵值對,因此可以快速可靠地解析、過濾、搜尋和分析。對於伺服器而言,日誌記錄是開發者觀察系統詳細行為的重要方式,通常也是他們進行除錯的首選之地。因此,日誌往往數量龐大,能夠快速搜尋和過濾它們至關重要。

自十多年前 Go 釋出以來,標準庫就有一個日誌記錄包 log。隨著時間的推移,我們瞭解到結構化日誌記錄對於 Go 程式設計師來說非常重要。在我們的年度調查中,它一直名列前茅,Go 生態系統中的許多包都提供了這一功能。其中一些非常流行:最早的 Go 結構化日誌記錄包之一,logrus,在超過 100,000 個其他包中使用。

有許多結構化日誌記錄包可供選擇,大型程式通常會透過其依賴項包含多個日誌包。主程式可能需要配置這些日誌包中的每一個,以使日誌輸出保持一致:所有日誌都輸出到同一個地方,採用相同的格式。透過在標準庫中包含結構化日誌記錄,我們可以提供一個所有其他結構化日誌記錄包都可以共享的通用框架。

slog 快速上手

下面是使用 slog 的最簡單程式

package main

import "log/slog"

func main() {
    slog.Info("hello, world")
}

截至本文撰寫時,它會列印

2023/08/04 16:09:19 INFO hello, world

Info 函式使用預設日誌記錄器在 Info 日誌級別列印訊息,在這種情況下,預設日誌記錄器是來自 log 包的預設日誌記錄器——與您編寫 log.Printf 時獲得的日誌記錄器相同。這解釋了為什麼輸出看起來如此相似:只有“INFO”是新的。開箱即用,slog 和原始的 log 包協同工作,使得入門變得容易。

除了 Info,還有用於其他三個級別——DebugWarnError——的函式,以及一個更通用的 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,產生我們上面看到的輸出。我們可以透過更改日誌記錄器使用的 handler 來更改輸出。slog 帶有兩個內建的 handler。一個 TextHandlerkey=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"}

您不限於使用內建的 handler。任何人都可以透過實現 slog.Handler 介面來編寫 handler。handler 可以生成特定格式的輸出,或者它可以包裝另一個 handler 來新增功能。slog 文件中的一個示例展示瞭如何編寫一個包裝 handler,用於更改顯示日誌訊息的最低級別。

我們目前使用的屬性的交替鍵值語法很方便,但對於頻繁執行的日誌語句,使用 Attr 型別並呼叫 LogAttrs 方法可能更有效率。它們協同工作以最大程度地減少記憶體分配。有一些函式可以從字串、數字和其他常見型別構建 Attr。此對 LogAttrs 的呼叫產生與上面相同的輸出,但速度更快

slog.LogAttrs(context.Background(), slog.LevelInfo, "hello, world",
    slog.String("user", os.Getenv("USER")))

slog 還有更多功能

  • 正如對 LogAttrs 的呼叫所示,您可以將 context.Context 傳遞給某些日誌函式,以便 handler 可以提取上下文資訊,例如跟蹤 ID。(取消上下文不會阻止日誌條目被寫入。)

  • 您可以呼叫 Logger.With 將屬性新增到日誌記錄器,這些屬性將出現在其所有輸出中,有效地提取出多個日誌語句的共同部分。這不僅方便,還可以幫助提高效能,如下文所述。

  • 屬性可以組合成組(group)。這可以為您的日誌輸出新增更多結構,並有助於區分原本相同的鍵。

  • 您可以透過為其型別提供 LogValue 方法來控制值在日誌中如何顯示。這可以用於將結構體的欄位記錄為組遮蔽敏感資料等。

瞭解 slog 所有功能的最佳位置是包文件

效能

我們希望 slog 速度快。為了獲得大規模的效能提升,我們設計了Handler 介面,以提供最佳化機會。在每個日誌事件開始時都會呼叫 Enabled 方法,使 handler 有機會快速丟棄不需要的日誌事件。WithAttrsWithGroup 方法允許 handler 對 Logger.With 新增的屬性進行一次格式化,而不是在每個日誌呼叫時都進行。當大型屬性(例如 http.Request)新增到 Logger 並隨後在許多日誌呼叫中使用時,這種預格式化可以顯著加快速度。

為了指導我們的效能最佳化工作,我們調查了現有開源專案中的典型日誌記錄模式。我們發現超過 95% 的日誌方法呼叫傳遞了五個或更少的屬性。我們還對屬性型別進行了分類,發現少數常見型別佔了大多數。然後,我們編寫了捕獲常見情況的基準測試,並將其作為指南來檢視時間都花在了哪裡。最大的收益來自於對記憶體分配的仔細關注。

設計過程

slog 包是自 Go 1 於 2012 年釋出以來標準庫中最大的新增功能之一。我們希望花時間來設計它,並且知道社群反饋將至關重要。

到 2022 年 4 月,我們收集了足夠的資料來證明結構化日誌記錄對 Go 社群的重要性。Go 團隊決定探索將其新增到標準庫中。

我們首先研究了現有結構化日誌記錄包的設計方式。我們還利用 Go module proxy 上儲存的大量開源 Go 程式碼,瞭解這些包的實際使用情況。我們的第一個設計參考了這項研究以及 Go 的簡潔精神。我們想要一個易於理解、簡潔明瞭的 API,同時又不犧牲效能。

取代現有的第三方日誌記錄包從來不是目標。它們在各自領域都做得很好,替換工作良好的現有程式碼很少能有效利用開發者的時間。我們將 API 分為前端 Logger,它呼叫後端介面 Handler。這樣,現有的日誌記錄包就可以與一個通用的後端通訊,因此使用它們的包無需重寫即可相互協作。許多常見日誌記錄包(包括 Zaplogrhclog)的 handler 已經編寫完成或正在進行中。

我們在 Go 團隊內部以及其他具有豐富日誌記錄經驗的開發者中分享了我們的初步設計。我們根據他們的反饋進行了修改,到 2022 年 8 月,我們認為已經有了可行的設計。8 月 29 日,我們將我們的實驗性實現公開,並開始了GitHub 討論,聽取社群的意見。社群反應熱烈且大體積極。感謝其他結構化日誌記錄包的設計者和使用者的深刻評論,我們進行了多處修改,並添加了一些功能,例如 groups 和 LogValuer 介面。我們將日誌級別到整數的對映關係更改了兩次。

經過兩個月和大約 300 條評論,我們覺得已經可以提出正式的提案和隨附的設計文件了。提案問題收到了超過 800 條評論,並對 API 和實現進行了許多改進。以下是兩個關於 context.Context 的 API 更改示例

  1. 最初的 API 支援將日誌記錄器新增到 context 中。許多人認為這是在不關心日誌的程式碼層級中方便地傳遞日誌記錄器的一種方式。但另一些人認為這偷偷引入了隱式依賴,使程式碼更難理解。最終,我們因其爭議過大而移除了該功能。

  2. 我們還糾結於向日志方法傳遞 context 的相關問題,嘗試了多種設計。最初我們抵制將 context 作為第一個引數的標準模式,因為我們不希望每個日誌呼叫都需要一個 context,但最終建立了兩套日誌方法,一套帶有 context,一套不帶。

我們未進行的一項更改涉及用於表達屬性的交替鍵值語法

slog.Info("message", "k1", v1, "k2", v2)

許多人強烈認為這是一個糟糕的主意。他們覺得這種方式難以閱讀,而且容易因遺漏鍵或值而出錯。他們更喜歡使用顯式屬性來表達結構

slog.Info("message", slog.Int("k1", v1), slog.String("k2", v2))

但我們認為,更輕量的語法對於保持 Go 易於使用且有趣非常重要,特別是對於 Go 新手而言。我們也知道,一些 Go 日誌記錄包,如 logrgo-kit/logzap(及其 SugaredLogger),成功地使用了交替鍵值。我們添加了一個vet 檢查來捕獲常見錯誤,但沒有改變設計。

2023 年 3 月 15 日,提案被接受,但仍有一些輕微的未解決問題。在接下來的幾周裡,又提出並解決了十項額外的更改。到 7 月初,log/slog 包的實現以及用於驗證 handler 的 testing/slogtest 包和用於檢查交替鍵值正確用法的 vet check 全部完成。

2023 年 8 月 8 日,Go 1.21 釋出,slog 也隨之釋出。我們希望您覺得它有用,並且使用它像構建它一樣有趣。

非常感謝所有參與討論和提案過程的人。你們的貢獻極大地改進了 slog

資源

log/slog 包的文件解釋瞭如何使用它並提供了幾個示例。

維基頁面包含 Go 社群提供的額外資源,包括各種 handler。

如果您想編寫一個 handler,請查閱handler 編寫指南

下一篇文章:完全可重現、可驗證的 Go 工具鏈
上一篇文章:Go 1.21 中的前向相容性和工具鏈管理
部落格索引