slog:Go 官方的結構化日誌包開發的怎麼樣了?該如何使用?
你好,我是小四,你情商高,也可以叫我四哥~
熟悉 Go 的同學都知道 Go 語言標準庫 log 有許多痛點,比如沒有日誌分級、沒有結構化(沒有 JSON 格式)、擴展性差等,爲了解決這些問題 Go 官方推出了結構化日誌包 slog,目前這個庫正在開發階段,已經進入了實驗庫:golang.org/x/exp/slog,目前版本是 v0.0.0。
這篇文章我們就來看下 slog 包怎麼用?
安裝
使用下面的命令安裝:
go get golang.org/x/exp/slog
開箱即用
func main() {
slog.Info("Go is best language!", "公衆號", "Golang來啦")
}
輸出:
2023/01/23 10:23:37 INFO Go is best language! 公衆號=Golang來啦
看輸出有點類似標準庫 log 的輸出。slog 庫裏一個非常重要結構體就是 Logger,通過它就可以調用日誌記錄函數 Info()、Debug() 等。這個我們沒有創建 Logger,會使用默認的,大家可以點進去看下源碼。
Handler
Handler 定義成一個接口,這可以讓 slog 的擴展性更強,slog 提供了兩個內置的 Handler 實現:TextHandler 和 JSONHandler,另外我們可以基於第三方 log 包定義或者自己定義 Handler 的實現,這個我們後面會講到。
type Handler interface {
Enabled(Level) bool
Handle(r Record) error
WithAttrs(attrs []Attr) Handler
WithGroup(name string) Handler
}
Text Handler
TextHandler 會像標準庫 log 包那樣將日誌以一行文本那樣輸出。
func main() {
textHandler := slog.NewTextHandler(os.Stdout)
logger := slog.New(textHandler)
logger.Info("Go is best language!", "公衆號", "Golang來啦")
}
輸出:
time=2023-01-23T10:48:41.365+08:00 level=INFO msg="Go is best language!" 公衆號=Golang來啦
我們看到,輸出的日誌以 “key1=value1 key2=value2 … keyN=valueN” 形式呈現。
JSON Handler
我們將上面的 NewTextHandler() 換成 NewJSONHandler()
func main() {
textHandler := slog.NewJSONHandler(os.Stdout)
logger := slog.New(textHandler)
logger.Info("Go is best language!", "公衆號", "Golang來啦")
}
輸出:
{"time":"2023-01-23T11:02:27.1606485+08:00","level":"INFO","msg":"Go is best language!","公衆號":"Golang來啦"}
從輸出可以看到,日誌已 json 格式記錄,這樣的結構化日誌非常適合機器解析。
日誌選項
日常開發中我們一般都會在日誌裏面記錄在哪個文件哪一行記錄了這條日誌,這樣有利於排查問題。或者,有時候需要更改日誌級別,那這些該怎麼實現呢?
如果我們翻看源碼就能發現,上面提到的 TextHandler 和 JSONHandler 都使用默認的 HandlerOptions,它是一個結構體。
type HandlerOptions struct {
AddSource bool
Level Leveler
ReplaceAttr func(groups []string, a Attr) Attr
}
通過 slog 的源代碼註釋可以看出,如果 AddSource 設置爲 true,則記錄日誌時會以 ("source", "file:line") 的方式記錄來源;Level 用於調整日誌級別。
默認情況下,slog 只會記錄 Info 及以上級別的日誌,不會記錄 Debug 級別的日誌。
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout))
logger.Debug("記錄日誌-debug",
"公衆號", "Golang來啦",
"time", time.Since(time.Now()))
logger.Info("記錄日誌-info",
"公衆號", "Golang來啦",
"time", time.Since(time.Now()))
}
輸出:
{"time":"2023-01-23T15:36:14.8610328+08:00","level":"INFO","msg":"記錄日誌-info","公衆號":"Golang來啦","time":0}
這樣的話,我們就可以自定義 option。
func main() {
opt := slog.HandlerOptions{ // 自定義option
AddSource: true,
Level: slog.LevelDebug, // slog 默認日誌級別是 info
}
logger := slog.New(opt.NewJSONHandler(os.Stdout))
logger.Debug("記錄日誌-debug",
"公衆號", "Golang來啦",
"time", time.Since(time.Now()))
logger.Info("記錄日誌-info",
"公衆號", "Golang來啦",
"time", time.Since(time.Now()))
}
輸出:
{"time":"2023-01-23T15:38:45.3747228+08:00","level":"DEBUG","source":"D:/examples/context/demo1/demo1.go:81","msg":"記錄日誌-debug","公衆號":"Golang來啦","time":0}
{"time":"2023-01-23T15:38:45.3949544+08:00","level":"INFO","source":"D:/examples/context/demo1/demo1.go:84","msg":"記錄日誌-info","公衆號":"Golang來啦","time":0}
從輸出可以看到記錄日誌的時候顯示了來源,同時也記錄了 debug 級別的日誌。
SetDefault() 設置默認 Logger
有一點值得注意的是,slog.SetDefault() 會將傳進來的 logger 作爲默認的 Logger,所以下面這兩行輸出是一樣的:
func main() {
textHandler := slog.NewJSONHandler(os.Stdout)
logger := slog.New(textHandler)
slog.SetDefault(logger)
logger.Info("Go is best language!", "公衆號", "Golang來啦")
slog.Info("Go is best language!", "公衆號", "Golang來啦")
}
輸出:
{"time":"2023-01-23T11:17:32.7518696+08:00","level":"INFO","msg":"Go is best language!","公衆號":"Golang來啦"}
{"time":"2023-01-23T11:17:32.7732035+08:00","level":"INFO","msg":"Go is best language!","公衆號":"Golang來啦"}
另外,如果設置裏默認的 Logger,調用 log 包方法時也會使用默認的:
func main() {
textHandler := slog.NewJSONHandler(os.Stdout)
logger := slog.New(textHandler)
slog.SetDefault(logger)
log.Print("something went wrong")
log.Fatalln("something went wrong")
}
輸出:
{"time":"2023-01-23T11:18:31.5850509+08:00","level":"INFO","msg":"something went wrong"}
{"time":"2023-01-23T11:18:31.6043829+08:00","level":"INFO","msg":"something went wrong"}
exit status 1
兩種記錄日誌的方式
通過 slog 包記錄日誌除了上面提到的這種方式:
logger.Info("Go is best language!", "公衆號", "Golang來啦")
這種方式會涉及到額外的內存分配,主要是爲了簡介設計的。
另外一種記錄日誌方式就像下面這樣:
logger.LogAttrs(slog.LevelInfo, "Go is best language!", slog.String("公衆號", "Golang來啦"))
這兩種輸出日誌格式都是一樣的,第二種爲了提高記錄日誌的性能而設計的,需要自己指定日誌級別、參數屬性 (以鍵值對的方式指定)。
目前 slog 包支持下面這些屬性:
String
Int64
Int
Uint64
Float64
Bool
Time
Duration
我們還可以多指定一些屬性:
logger.LogAttrs(slog.LevelInfo, "Go is best language!", slog.String("公衆號", "Golang來啦"), slog.Int("age", 18))
輸出:
{"time":"2023-01-23T11:45:11.7921124+08:00","level":"INFO","msg":"Go is best language!","公衆號":"Golang來啦","age":18}
如何綁定一組屬性
學到這裏我就在想,假如我想在一個 key 下面綁定一組 key-value 值該怎麼做呢?這種需求在日常開發中是很常見的,我翻了翻源碼,slog 還真的提供了相關方法 -- slog.Group()。
func main() {
textHandler := slog.NewJSONHandler(os.Stdout)
logger := slog.New(textHandler)
slog.SetDefault(logger)
logger.Info("Usage Statistics",
slog.Group("memory",
slog.Int("current", 50),
slog.Int("min", 20),
slog.Int("max", 80)),
slog.Int("cpu", 10),
slog.String("app-version", "v0.0.0"),
)
}
輸出:
{"time":"2023-01-23T13:45:26.9179901+08:00","level":"INFO","msg":"Usage Statistics","memory":{"current":50,"min":20,"max":80},"cpu":10,"app-version":"v0.0.0"}
memory 元素下面對應不同的 key-value。
如何綁定公共的屬性
日常開發中,可能會遇到每一條日誌需要記錄一些相同的公共信息,比如 app-version。
...
logger.Info("Usage Statistics",
slog.Group("memory",
slog.Int("current", 50),
slog.Int("min", 20),
slog.Int("max", 80)),
slog.Int("cpu", 10),
slog.String("app-version", "v0.0.0"),
)
logger.Info("記錄日誌",
"公衆號", "Golang來啦",
"time", time.Since(time.Now()), slog.String("app-version", "v0.0.0"))
...
如果想上面這樣,每次都記錄一次 app-version 的話就有點繁瑣了。好在 slog 自帶的 TextHandler 和 JSONHandler 提供了 WithAttrs() 方法可以實現綁定公共屬性。
func main() {
textHandler := slog.NewJSONHandler(os.Stdout).WithAttrs([]slog.Attr{slog.String("app-version", "v0.0.0")})
logger := slog.New(textHandler)
slog.SetDefault(logger)
logger.Info("Usage Statistics",
slog.Group("memory",
slog.Int("current", 50),
slog.Int("min", 20),
slog.Int("max", 80)),
slog.Int("cpu", 10),
)
logger.Info("記錄日誌",
"公衆號", "Golang來啦",
"time", time.Since(time.Now()))
}
輸出:
{"time":"2023-01-23T14:01:46.2845325+08:00","level":"INFO","msg":"Usage Statistics","app-version":"v0.0.0","memory":{"current":50,"min":20,"max":80},"cpu":10}
{"time":"2023-01-23T14:01:46.303597+08:00","level":"INFO","msg":"記錄日誌","app-version":"v0.0.0","公衆號":"Golang來啦","time":0}
從輸出可以看到兩條日誌都記錄了 app-version,這種記錄方式就簡潔多了。
通過 context 存儲或提取 Logger
slog 的 Logger 還與 context.Context 結合在一起,比如通過 slog.WithContext() 存儲 Logger、通過 slog.FromContext() 提取 Logger。這樣我們就可以在不同函數之間通過 context 傳遞 Logger。
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout))
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
l := logger.With("path", r.URL.Path).With("user-agent", r.UserAgent()) // With() 綁定額外的信息
ctx := slog.NewContext(r.Context(), l) // 生成 context
handleRequest(w, r.WithContext(ctx))
})
http.ListenAndServe(":8080", nil)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
logger := slog.FromContext(r.Context()) // 提取 Logger
logger.Info("handling request",
"status", http.StatusOK)
w.Write([]byte("Hello World"))
}
執行程序並訪問地址: http://127.0.0.1:8080/hello
輸出:
{"time":"2023-01-23T14:36:26.6303067+08:00","level":"INFO","msg":"handling request","path":"/hello","user-agent":"curl/7.83.1","status":200}
上面這種使用 Logger 的方式是不是還挺方便的,不過很遺憾的是,在最新的 slog 包裏,這兩個方法已經被作者移除掉了。
我很好奇作者爲什麼把這兩個方法移除掉,後面翻到 slog 提案 [1] 下面作者留言 [2],大意是說這種使用方式有比較大的爭議 (主要是函數之間能否使用 context),而且如果使用者喜歡這種使用方式的話,也可以自己實現,所以把這兩個方法移除了。
如果需要自己實現通過 context 儲存和提取 Logger,你知道怎麼實現嗎?歡迎留言區交流,嘻嘻。
如何集成第三方日誌包
在講 Handler 那一節時提到過,如果我們實現了 Handler 接口,就可以將第三方 log 與 Logger 集成,那該怎麼實現呢?我們就拿 logrus 日誌包舉例吧。
package main
import (
"fmt"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slog"
"net"
"net/http"
"os"
)
func init() {
// 設置logrus
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.SetOutput(os.Stdout)
logrus.SetLevel(logrus.DebugLevel)
}
func main() {
// 將 Logrus 與 Logger 集成在一塊
logger := slog.New(&LogrusHandler{
logger: logrus.StandardLogger(),
})
logger.Error("something went wrong", net.ErrClosed,
"status", http.StatusInternalServerError)
}
type LogrusHandler struct {
logger *logrus.Logger
}
func (h *LogrusHandler) Enabled(_ slog.Level) bool {
return true
}
func (h *LogrusHandler) Handle(rec slog.Record) error {
fields := make(map[string]interface{}, rec.NumAttrs())
rec.Attrs(func(a slog.Attr) {
fields[a.Key] = a.Value.Any()
})
entry := h.logger.WithFields(fields)
switch rec.Level {
case slog.LevelDebug:
entry.Debug(rec.Message)
case slog.LevelInfo:
entry.Info(rec.Message)
case slog.LevelWarn:
entry.Warn(rec.Message)
case slog.LevelError:
entry.Error(rec.Message)
}
fmt.Println("測試是否走了這個方法:記錄日誌")
return nil
}
func (h *LogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
// 爲了演示,此方法就沒有實現,但不影響效果
return h
}
func (h *LogrusHandler) WithGroup(name string) slog.Handler {
// 爲了演示,此方法就沒有實現,但不影響效果
return h
}
輸出:
{"err":"use of closed network connection","level":"error","msg":"something went wrong","status":500,"time":"2023-01-23T16:07:40+08:00"}
測試是否走了這個方法:記錄日誌
追查代碼發現,通過調用 slog 的方法記錄日誌時都會調用 logPC() 方法生成一條 Record,最終會交給 Handler 接口的具體實現方法 Handle(),這裏就是我們自己實現的方法
func (h *LogrusHandler) Handle(rec slog.Record) error {}
從輸出就可以看出,最終調用了自己實現的 Handle() 方法,走的是 logrus 包的方法 entry.Error()。
總結
這篇文章主要介紹了 slog 包的一些主要方法的使用,簡單說了下里面一些函數、方法的實現,更詳細的細節大家可以自行查看源碼。目前中文社區關於 slog 的文章不多 (可能是我沒發現,歡迎補充),我發現比較好的已經在底部的參考文章裏列出來了,作爲補充可以深入瞭解 slog 包。另外感興趣的同學可以看下關於 slog 的提案(裏面會實時更新一些信息以及社區開發者的討論) 和 slog 包的設計文檔,具體鏈接看參考文章。歡迎留言交流,一起學習成長。
參考資料
[1]
提案: https://github.com/golang/go/issues/56345
[2]
留言: https://github.com/golang/go/issues/56345#issuecomment-1381910606
[3]
slog:Go 官方版結構化日誌包: https://tonybai.com/2022/10/30/first-exploration-of-slog/
[4]
proposal: log/slog: structured, leveled logging: https://github.com/golang/go/issues/56345
[5]
Proposal: Structured Logging: https://go.googlesource.com/proposal/+/master/design/56345-structured-logging.md
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/jz7LCEpoKXn-QmMSsisBgg