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 是零,Warn 是四,因此如果您的日誌記錄系統在這兩者之間有一個級別,您可以使用二。

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 附帶兩個內建處理程式。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"}

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

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

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

slog 還有很多內容

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

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

  • 屬性可以組合成組。這可以為您的日誌輸出新增更多結構,並有助於消除可能相同的鍵的歧義。

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

瞭解 slog 所有功能的最佳地點是程式包文件

效能

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

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

設計過程

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

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

我們首先研究了現有結構化日誌包的設計方式。我們還利用儲存在 Go 模組代理上的大量開源 Go 程式碼來了解這些包的實際使用情況。我們的第一個設計受到了這項研究以及 Go 追求簡潔精神的影響。我們希望有一個 API,它在程式碼中輕巧易懂,同時又不犧牲效能。

我們從未有過替換現有第三方日誌包的目標。它們各自都做得很好,替換現有的執行良好的程式碼很少能成為開發人員時間的好去處。我們將 API 分為呼叫後端介面 Handler 的前端 Logger。這樣,現有的日誌包就可以與一個通用的後端進行通訊,因此使用它們包的程式可以互操作而無需重寫。針對許多常見的日誌包,例如 Zaplogrhclog,正在編寫或正在開發相應的處理程式。

我們在 Go 團隊和其他有豐富日誌記錄經驗的開發人員中分享了我們的初步設計。我們根據他們的反饋進行了修改,到 2022 年 8 月,我們認為我們有了一個可行的設計。8 月 29 日,我們公開了我們的實驗性實現,並開始了一個GitHub 討論,以聽取社群的意見。反響非常熱烈,絕大多數是積極的。感謝其他結構化日誌包的設計者和使用者的富有洞察力的評論,我們進行了一些修改並添加了一些功能,例如組和 LogValuer 介面。我們兩次更改了日誌級別到整數的對映。

經過兩個月和大約 300 條評論,我們認為我們已經準備好進行實際的提案和配套的設計文件。提案問題獲得了 800 多條評論,並對 API 和實現進行了許多改進。以下是兩個 API 更改的示例,都與 context.Context 有關。

  1. 最初,API 支援將日誌記錄器新增到上下文中。許多人認為這是將日誌記錄器輕鬆傳遞給不關心它的程式碼層的一種便捷方式。但也有人認為這是一種隱藏的隱式依賴,使程式碼更難理解。最終,我們因為爭議過大而移除了該功能。

  2. 我們還圍繞將上下文傳遞給日誌記錄方法的問題進行了爭論,嘗試了多種設計。我們最初抵制將上下文作為第一個引數傳遞的標準模式,因為我們不希望每個日誌呼叫都需要上下文,但最終建立了兩組日誌記錄方法,一組帶上下文,一組不帶上下文。

我們沒有做出的一項更改涉及用於表達屬性的交替鍵值語法

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

許多人堅決認為這是一個壞主意。他們發現它難以閱讀,並且很容易因為省略鍵或值而出錯。他們更喜歡使用顯式屬性來表達結構。

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

但我們認為,這種更輕量級的語法對於保持 Go 的易用性和趣味性至關重要,特別是對於新的 Go 程式設計師。我們還知道,像 logrgo-kit/logzap(帶有其 SugaredLogger)等幾個 Go 日誌包成功地使用了交替的鍵和值。我們添加了一個vet 檢查來捕獲常見錯誤,但沒有更改設計。

2023 年 3 月 15 日,提案獲得透過,但仍有一些小問題懸而未決。在接下來的幾周裡,又提出了並解決了十項附加更改。到 7 月初,log/slog 包的實現以及用於驗證處理程式的 testing/slogtest 包和用於正確使用交替鍵值的 vet 檢查都已完成。

8 月 8 日,Go 1.21 釋出,slog 也隨之釋出。希望您發現它很有用,並且使用起來像構建它一樣有趣。

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

資源

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

Wiki 頁面 包含 Go 社群提供的其他資源,包括各種處理程式。

如果您想編寫處理程式,請參閱處理程式編寫指南

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