Go 十年了,終於想起要統一 log 庫了!

語言: CN / TW / HK

大家好,我是煎魚。

在日常工作中,打日誌是很常見的動作。畢竟不打日誌,從內部來講,一旦出問題,定位、排查都會變的非常困難。誰也不想大半夜在那靠猜解決問題。

在其他方面,對日誌的儲存的內容、時長、安全均有不同程度的合規要求,應對客戶訴求和提單上門的事件。

日誌好不好用,就成了重要的訴求了。

標準庫 log 很痛

思考一個問題:平時你在寫 Go 工程時,是否很少直接使用官方標準庫 log?

在正式專案中,大多是優先使用幾個爆款第三方庫,例如:Logrus、Zap、zerolog。而標準庫 log,在臨時除錯,螢幕輸出的場景居多,佔比較少。

這問題出在了哪裡?主要集中在以下方面: - 沒有日誌分級。不便於分類、定位、排查問題,例如:Error、Warn、Info、Debug 等。 - 沒有結構化日誌。只提供格式化日誌,不提供結構化,不便於程式讀取、解析,例如:Json 格式。 - 沒有擴充套件性,靈活度差。標準庫 log 的日誌輸出都是固定格式,沒有一個 Logger 介面規範,讓大家都遵守,以至於現在社群純自然演進,難互相相容。

除此之外,在使用者場景上,有著不包含上下文(context)資訊、效能不夠強勁、無法引入自定義外掛等擴充套件訴求。基本上第三方庫均有實現的,基本都使用者的痛點之一。

為什麼不早點解決

你可能會想,標準庫 log 作為 Go 生態裡的核心庫,為什麼不早點解決?

實際上在 2017 年時,有在社群進行了大規模討論,可惜放棄了。原因是:“我們還沒有找到足夠多的匯入和使用具體 Logger 的 Go 庫,因此沒有理由繼續開展這項工作”。

如下圖:

g/golang-dev/c/F3l9Iz1JX4g/m/t0J0loRaDQAJ

繼續擺爛。

救星 slog 庫誕生

討論和目標

在 2022 年 8 月,Go 團隊的 @ Jonathan Amsterdam 發起了 discussion: structured, leveled logging 的討論,試圖與這個亂象再度一決雌雄。

discussions/54763

提案(含討論)的目標是: - 使用方便。對現有 Logger 庫的調查說明,開發人員更想要一個簡潔且易懂的日誌 API。 - 高效能。新的 API 希望做到最大限度的減少記憶體分配和鎖定。 - 與執行時跟蹤整合。Go 團隊正在開發和改進執行時跟蹤系統,基於新 Logger 庫的日誌將可以無縫銜接到這個跟蹤系統中,開發人員能夠實現程式操作與執行時的行為相關聯。

目標涵蓋了前文背景中提到的痛點。我關注到上述的第三點,來自 Go 團隊自己的需求,果然最優先要做的需求都是自己想要 PUSH 的需求?霧了霧了。

畢竟已經 10 年了,本討論中得到了許多人的建議和推進,成功孵化。

快速 Demo

該庫目前已經經過 “石錘” 階段,進入了實驗庫,匯入地址是:golang.org/x/exp/slog。

我們先上手新日誌庫 slog 的快速 Demo,便於大家快速瞭解和熟悉。

如下程式碼:

```go import "log/slog"

func main() { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr))) slog.Info("hello", "name", "Al") slog.Error("oops", net.ErrClosed, "status", 500) slog.LogAttrs(slog.ErrorLevel, "oops", slog.Int("status", 500), slog.Any("err", net.ErrClosed)) } ```

如果不設定 slog.SetDefault 將會預設輸出到標準輸出。由於上述程式設定了 os.Stderr,因此會在此輸出。

程式結果如下:

time=2022-10-24T16:05:48.054-04:00 level=INFO msg=hello name=Al time=2022-10-24T16:05:48.054-04:00 level=ERROR msg=oops status=500 err="use of closed network connection" time=2022-10-24T16:05:48.054-04:00 level=ERROR msg=oops status=500 err="use of closed network connection"

我們已經看到了日誌分級(Level)、自定義欄位追加、設定輸出地等特性。在輸出格式上,新的 slog 庫,將會採取與 logfmt 庫類似的方式來實現,內建至少兩種格式。

預設的 logfmt 訊息格式:

foo=bar a=14 baz="hello kitty" cool%story=bro f %^asdf

如果想調整為 JSON 格式,可進行設定:

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr)))

會使用 JSON 格式輸出:

json { "foo": "bar", "a": 14, "baz": "hello kitty", "cool%story": "bro", "f": true, "%^asdf": true }

設計思路

作者將 slog 庫的設計分為:前端、後端。

前端,slog 認為你常用且能看得見的 API 都是前端,例如:Info、Debug 等日誌分級的,設定上下文內容的 Context 和自定義欄位注入等都包含在前端的範疇內。

如下方法:

後端,slog 認為實際幹具體業務邏輯的 Handler 是後端,並將其抽象成了 Handler 介面,只需要實現 Handler 介面,就可以注入自定義 Handler。

如下 Handler 介面:

```go type Handler interface { // 啟用記錄的日誌級別 Enabled(Level) bool // 具體的處理方法,需要 Enabled 返回 true Handle(r Record) error

WithAttrs(attrs []Attr) Handler

WithGroup(name string) Handler

} ```

其中你可以看到 Handle 函式有一個 Record 屬性,它是一個核心的資料結構。

如下程式碼:

```go type Record struct { Time time.Time

Message string

Level Level

Context context.Context

} ```

新的 slog 的內部流程如下: 1. 前端方法(例如:Info)將所傳屬性封裝為 Record 型別的變數。 2. 將 Record 型別的變數傳遞給後端方法(例如:Handle)。 3. 後端 Handle 方法根據所得 Record,進行對應的格式化、方法呼叫、日誌輸出。

與其他 Logger 互動

那回到最開始的問題?

如果我們現在要寫一個私有的 Logger,或是複用 Zap。要怎麼做?

後端方法,有兩條路(同一條路): 1. 要不走 Record,呼叫 NewRecord 將其包裝成 Record 型別的變數,再往下傳。 2. 要不走 Handle,將處理邏輯寫到自定義 Handle 中去完成。

如果是想在前端方法來處理,很遺憾,Go 沒有計劃將 slog 前端開放。確保了前端穩態,後端可變可擴充套件的靈活性。

如果有興趣瞭解如何實現自定義 Handle,可以檢視 TextHandlerJSONHandler 即可,是官方最佳實踐。

上下文注入

經典的 context 場景,slog 庫直接內建了相關的函式進行支援。

如下程式碼:

```go func FromContext(ctx context.Context) Logger FromContext returns the Logger stored in ctx by NewContext, or the default Logger if there is none.

func NewContext(ctx context.Context, l Logger) context.Context NewContext returns a context that contains the given Logger. Use FromContext to retrieve the Logger. ```

具體的 Demo:

go func handle(w http.ResponseWriter, r *http.Request) { rlogger := slog.FromContext(r.Context()).With( "method", r.Method, "url", r.URL, "traceID", getTraceID(r), ) ctx := slog.NewContext(r.Context(), rlogger) // ... use slog.FromContext(ctx) ... }

還是比較方便的。

總結

在此刻,Go 社群中的 log 庫們已經基本成熟,格局已定的 7788。此時 Go 官方的 slog 庫推出,很明顯吸取了前者的大量豐富經驗(提案有宣告)。

我相信在未來 slog 庫,會和更多的 Go 生態的工具鏈打通,提供更豐富的關聯場景。解決 Go 沒有一個靠譜 log 庫的痛點。

你覺得這個新庫對你有幫助嗎?

文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。

推薦閱讀

Go 圖書系列