探索 Go slog 標準庫:設計與應用
本文主要介紹了 Go 語言新引入的
log/slog
標準庫的設計理念、使用方法以及如何進行定製化開發,以提高日誌操作的性能和靈活性。原文: Explore the Go slog Standard Library: Design and Usage[1]
在 Go1.21[2] 中引入的 log/slog[3] 軟件包試圖彌補原有日誌軟件包的不足,即日誌缺乏結構化和級別特性。正如提案 [4] 中提到的,log/slog 包旨在創建一個零依賴、易用、高性能、靈活高效的日誌系統。如果你對它的概念感興趣,並想更好的利用它,那就請跟隨我深入瞭解它的設計、實現和應用。
slog 設計
爲實現其目的,slog 的設計具有高度靈活性。它支持結構化日誌,能以 JSON 或其他格式輸出日誌,以便後續分析和處理。出色的模塊化設計將日誌功能分爲多個組件,如日誌級別管理、輸出格式化、日誌傳輸等,每個組件都可以獨立配置和替換。其異步處理支持機制可確保日誌操作不會阻礙主應用程序的執行。
// Logger contains all actions against a Log message
type Logger struct {
New(h Handler) *Logger
Debug(msg string, args ...any)
Error(msg string, args ...any)
Info(msg string, args ...any)
//...
With(args ...any) *Logger
}
// Handler defines how to
type Handler interface {
Handle(context.Context, Record) error
WithAttrs(attrs []Attr) Handler
WithGroup(name string) Handler
}
// Record defines a Log message with many info。
type Record struct {
Time time.Time
Message string
Level Level
PC uintptr
}
三個組件完成了整個流程。
Logger
負責所有日誌打印操作。Handler
定義日誌輸出格式,默認實現爲 TextHandler
和 JSONHandler
。Record
定義日誌的詳細信息,如時間、級別等。這些接口不僅能以多種方式擴展 slog(如添加新的處理程序),還能滿足不同日誌框架的需求和配置。
slog 用法
slog 的基本用法與 zap 或 Golang 的其他第三方日誌框架類似。
日誌打印
func main() {
slog.Info("This is an informational message.")
slog.Warn("This is a warning message with context.", slog.String("user", "slaise"))
slog.Error("This is an error message with details.", slog.Int("code", 123))
}
// output(go-playground) https://go.dev/play/p/TLIur7rZFhi
2009/11/10 23:00:00 INFO This is an informational message.
2009/11/10 23:00:00 WARN This is a warning message with context. user=slaise
2009/11/10 23:00:00 ERROR This is an error message with details. code=123
slog 爲不同級別的日誌提供了不同的打印方式和相應的參數類型轉換。
Handler
通過下面的代碼,我們可以將默認打印方式修改爲內置的 TextHandler
或 JSONHandler
。
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// logger.Info/Debug/Error
請看輸出結果。
// JSONHandler
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("This is an informational message.")
logger.Warn("This is a warning message with context.", slog.String("user", "slaise"))
logger.Error("This is an error message with details.", slog.Int("code", 123))
}
// Output: https://go.dev/play/p/oCubHk77Sjw
{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"This is an informational message."}
{"time":"2009-11-10T23:00:00Z","level":"WARN","msg":"This is a warning message with context.","user":"slaise"}
{"time":"2009-11-10T23:00:00Z","level":"ERROR","msg":"This is an error message with details.","code":123}
// TextHandler
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
// Output: https://go.dev/play/p/ewil9ziZpsk
time=2009-11-10T23:00:00.000Z level=INFO msg="This is an informational message."
time=2009-11-10T23:00:00.000Z level=WARN msg="This is a warning message with context." user=slaise
time=2009-11-10T23:00:00.000Z level=ERROR msg="This is an error message with details." code=123
可以通過 SetDefault[5] 方法創建 Logger 來替換 slog 中的默認 Logger,默認 slog.Info
和 slog.Debug
的 Logger 也將同時被修改。
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
slog.Info("This is an informational message.")
slog.Warn("This is a warning message with context.", slog.String("user", "slaise"))
slog.Error("This is an error message with details.", slog.Int("code", 123))
}
//output
{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"This is an informational message."}
{"time":"2009-11-10T23:00:00Z","level":"WARN","msg":"This is a warning message with context.","user":"slaise"}
{"time":"2009-11-10T23:00:00Z","level":"ERROR","msg":"This is an error message with details.","code":123}
消息處理
對 Record
和 attr
的處理是 slog 核心功能的一部分。每條日誌記錄都由時間、級別、消息等參數和一組鍵值對組成。處理這些參數的 API 提供了靈活性。使用 With
方法,用戶可以輕鬆添加固定屬性,這些屬性將出現在該日誌記錄器生成的每條日誌記錄中。通過 Group
方法,用戶可以彙總多個屬性,進行統一處理。請看下面的示例。
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
// Build a group
attrGroup := slog.Group("ops", slog.String("module", "authentication"), slog.String("method", "login"))
logger.Info("User login attempt",
slog.String("service", "login-service"),
slog.String("version", "1.0.2"),
slog.String("status", "attempting"),
attrGroup,
)
// login failed
// failed, err := login(user, pass)
err := errors.New("login password err")
if err != nil {
// error can only be printed with slog.Any
logger.Error("User login failed",
slog.String("service", "login-service"),
slog.String("version", "1.0.2"),
slog.Any("error", err)) //output: service=login-service version=1.0.2 error="login password err
}
}
Attr[6] 提供了在 slog 中處理參數的主要 API,例如上例中的 slog.String
和 slog.Any
,以及 slog.Bool[7]、slog.Int[8]、slog.Float64[9]、slog.Duration[10]、slog.Time[11] 等。
Level 通過實現 Leveler[12] 接口的 Level()
爲 slog 提供默認日誌級別設置。
const (
LevelDebug Level = -4
LevelInfo Level = 0
LevelWarn Level = 4
LevelError Level = 8
)
func (l Level) Level() Level { return l }
日誌應用本地機器的默認時區設置,但也可以使用 slog.Time
添加特定時間到日誌中。
loc, err := time.LoadLocation("America/New_York")
currentTime := time.Now().In(loc)
slog.Info("Log message with NewYork timezone",
slog.Time("time", currentTime),
)
不過,如果我們需要在不同於本地機器的時區打印日誌,就需要自定義處理程序,因爲 slog 軟件包缺少用於全局修改的默認 API。
上下文處理
可以通過兩種方式將上下文添加到日誌記錄中。
-
通過在日誌處理中加入
context.Context
,可以在函數和 goroutine 之間傳遞上下文信息。 -
Logger 提供了帶
context
的日誌打印方法,如 DebugContext[13]、InfoContext[14]、WarnContext[15] 和 ErrorContext[16]。context.Context
被添加到之前的方法參數列表中,並將ctx
放在首位,這符合上下文的使用原則。
func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any)
但如果直接使用這些方法將context
中的參數添加到日誌中,就會像我一樣感到困惑,因爲這樣做是行不通的。例如,在以下代碼中,userId
不會顯示在日誌中。
func main() {
ctx := context.WithValue(context.Background(), "userId", "123")
slog.InfoContext(ctx, "reset password", slog.Time("time", time.Now()))
}
// output
2009/11/10 23:00:00 INFO reset password time=2009-11-10T23:00:00.000Z
閱讀源代碼後,你會驚訝的發現,默認 defaultHandler
只打印消息和參數列表,卻不處理傳入的 ctx。
func (h *defaultHandler) Handle(ctx context.Context, r Record) error {
buf := buffer.New()
buf.WriteString(r.Level.String())
buf.WriteByte(' ')
buf.WriteString(r.Message)
state := h.ch.newHandleState(buf, true, " ")
defer state.free()
state.appendNonBuiltIns(r)
return h.output(r.PC, *buf)
}
go 1.21 中正式發佈的 slog 只提供了相關 API 而沒有提供實現,因此需要定製處理程序來實現相關功能。
- 可通過父日誌記錄器構建具有層次關係的日誌記錄器,這些日誌記錄器繼承並擴展了基本日誌記錄功能
例如,在前面的With
和Group
示例中,在打印 Info 和 Error 日誌的過程中重複傳遞了服務和版本信息,而日誌程序的繼承功能可以簡化這一過程。
// Define a new Logger with service & service info
logger = slog.With(logger, slog.String("service", "login-service"), slog.String("version", "1.0.2"))
// Print logs
logger.Info("User login attempt",
slog.String("status", "attempting"),
attrGroup,
)
slog 定製
Slog 的設計非常靈活,便於定製日誌。
自定義日誌級別
與官方 LogLevel
一樣,可以通過實現 Logeler
接口來定製 LogLevel
,而 slog.Level
本身也會返回 Level 類型。
const (
LevelTrace = slog.Level(-99)
LevelFatal = slog.Level(99)
)
func main() {
ctx := context.Background()
// This will be hidden because default Log level is above trace
slog.Log(ctx, LevelTrace, "Trace message")
slog.Log(ctx, LevelFatal, "Fatal level")
}
// output
2009/11/10 23:00:00 ERROR+91 Fatal level
跟蹤級別日誌在未修改相應HandlerOptions
時是隱藏的,可通過修改默認日誌級別打開。
slog.SetLogLoggerLevel(LevelTrace)
自定義 HandlerOptions HandlerOptions[17] 提供三種修改日誌的方法。
-
顯示源代碼。使用
AddSource:true
,可以在日誌中添加文件、行號和方法名稱等信息。 -
修改日誌級別。
LevelTrace
的作用與上述SetLogLoggerLevel
相同。 -
定義
ReplaceAttr
可調用函數,用於修改日誌中的Attr
鍵值對。
對於缺少默認格式化支持的錯誤,可以通過實現 ReplaceAttr
來加以改進。
func replaceAttr(_ []string, a slog.Attr) slog.Attr {
switch a.Value.Kind() {
case slog.KindAny:
switch v := a.Value.Any().(type) {
case error:
a.Value = fmtErr(v) // provide error log format
}
}
return a
}
自定義 Handler 自定義日誌的最後一種方法是實現自己的 Handler。下面的 ContextHandler
提供了將上下文中的參數整合到日誌中的功能。
const (
UserId string = "userId"
)
type ContextHandler struct {
slog.Handler
}
func (h ContextHandler) Handle(ctx context.Context, r slog.Record) error {
if id, ok := ctx.Value(UserId).(string); ok {
r.AddAttrs(slog.String(UserId, id))
}
return h.Handler.Handle(ctx, r)
}
func main() {
ctxHandler := &ContextHandler{slog.NewJSONHandler(os.Stdout, nil)}
logger := slog.New(ctxHandler)
ctx := context.WithValue(context.Background(), "userId", "123")
logger.InfoContext(ctx, "User ops", slog.String("op", "login"))
}
//output
{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"User ops","op":"login","userId":"123"}
性能
slog 支持通過延遲計算優化參數處理,只在實際需要日誌信息時才執行參數的高性能字符串操作,以減少不必要的性能開銷。
在默認日誌方法中,只打印默認級別以上的日誌。Attrs
將被添加到打印前創建的Record
中。
func (l *Logger) log(ctx context.Context, level Level, msg string, args ...any) {
if !l.Enabled(ctx, level) {
return
}
var pc uintptr
if !internal.IgnorePC {
var pcs [1]uintptr
// skip [runtime.Callers, this function, this function's caller]
runtime.Callers(3, pcs[:])
pc = pcs[0]
}
r := NewRecord(time.Now(), level, msg, pc)
r.Add(args...)
if ctx == nil {
ctx = context.Background()
}
_ = l.Handler().Handle(ctx, r)
}
slog 利用內置 buffer.go[18] 中的對象池技術重複使用日誌條目對象,減少了內存應用和釋放以及垃圾回收的頻率,從而提高了應用性能。
slog 實踐
到目前爲止,我們已經做好了在項目中使用 slog 的準備,但要牢記以下幾點。
- 對敏感數據脫敏
數據安全在日誌處理中至關重要。在將對象對象打印到日誌時,應及時屏蔽或模糊敏感信息,以避免數據泄漏,這可以通過相應結構體中的 LogValue
接口來實現。下面我們來看一個包含電子郵件和密碼的用戶類型示例。
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Password string `json:"password"`
}
func (u User) LogValue() slog.Value {
return slog.StringValue(u.ID)
}
func main() {
h := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(h)
u := &User{
ID: "1",
Email: "slaise@gmail.com",
Password: "111111",
}
logger.Info("info", "user", u)
}
// output
{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"info","user":"1"}
- 將 slog 級別提取爲配置項,併爲不同環境配置不同級別
項目通常部署在多個環境中,如 dev
、staging
、prod
等。通過將日誌級別提取爲環境參數,並通過 Kubernetes 的 configMap
注入,可以在不同環境中應用不同日誌級別,從而減少開銷。
- 處理上下文信息
上例中的上下文實現是一個明智的選擇,可以通過優化來動態加載上下文參數列表。在我們實現的代碼及許多第三方日誌框架中,Context 被廣泛用於傳遞上下文信息,從而簡化了從第三方庫升級到 slog 的過程。
- 正確配置日誌輸出目的地
根據需要配置日誌輸出,如輸出到控制檯、文件、網絡服務等,或輸出到多個輸出目的地,以確保日誌的可靠存儲。
通過自定義處理程序的寫入器,可以輕鬆修改日誌輸出地址。
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("cannot open log file: " + err.Error())
}
defer file.Close()
logger := slog.New(slog.JSONHandler(file))
升級第三方庫
活躍的 Golang 社區有許多爲 slog 定製的升級框架,以下是一些常用框架。
-
slog-multi[19]:處理器鏈,如流水線、路由器、扇出等。
-
slog-sampling[20]:丟棄重複的日誌條目。
-
slog-shim[21] 爲 1.21 以下的 Go 版本提供向後兼容的 slog 支持。
-
sloggen[22] 生成各種輔助工具。
-
sloglint[23] 可確保代碼的一致性。
結論
slog
軟件包爲管理應用程序日誌提供了強大而靈活的解決方案。其設計確保了高性能和多功能性,使開發人員能夠自定義日誌級別、Handler 配置以及 Handler 本身。使用 slog
不僅能加強消息處理和上下文信息集成,還能簡化日誌記錄流程。爲了有效實施,仔細考慮自定義日誌級別和 Handler 以滿足特定項目要求至關重要。通過明智利用 slog
的功能,開發人員可以顯著提高日誌實踐的效率和清晰度,確保代碼具有更好的可維護性和可讀性。
你好,我是俞凡,在 Motorola 做過研發,現在在 Mavenir 做技術工作,對通信、網絡、後端架構、雲原生、DevOps、CICD、區塊鏈、AI 等技術始終保持着濃厚的興趣,平時喜歡閱讀、思考,相信持續學習、終身成長,歡迎一起交流學習。爲了方便大家以後能第一時間看到文章,請朋友們關注公衆號 "DeepNoMind",並設個星標吧,如果能一鍵三連 (轉發、點贊、在看),則能給我帶來更多的支持和動力,激勵我持續寫下去,和大家共同成長進步!
參考資料
[1]
Explore the Go slog Standard Library: Design and Usage: https://blog.stackademic.com/explore-the-go-slog-standard-library-design-and-usage-6baf14c03299
[2]
Go1.21: https://tip.golang.org/doc/go1.21#slog
[3]
log/slog: https://pkg.go.dev/log/slog
[4]
structured, leveled logging #54763: https://github.com/golang/go/discussions/54763
[5]
SetDefault: https://pkg.go.dev/log/slog#SetDefault
[6]
Attr: https://pkg.go.dev/log/slog#Attr
[7]
slog.Bool: https://pkg.go.dev/log/slog#Bool
[8]
slog.Int: https://pkg.go.dev/log/slog#Int
[9]
slog.Float64: https://pkg.go.dev/log/slog#Float64
[10]
slog.Duration: https://pkg.go.dev/log/slog#Duration
[11]
slog.Time: https://pkg.go.dev/log/slog#Time
[12]
Leveler: https://pkg.go.dev/log/slog#Leveler
[13]
DebugContext: https://pkg.go.dev/log/slog#Logger.DebugContext
[14]
InfoContext: https://pkg.go.dev/log/slog#Logger.InfoContext
[15]
WarnContext: https://pkg.go.dev/log/slog#Logger.WarnContext
[16]
ErrorContext: https://pkg.go.dev/log/slog#Logger.ErrorContext
[17]
HandlerOptions: https://pkg.go.dev/log/slog#HandlerOptions
[18]
buffer.go: https://cs.opensource.google/go/go/+/master:src/log/slog/internal/buffer/buffer.go;bpv=0;bpt=1
[19]
slog-multi: https://github.com/samber/slog-multi
[20]
slog-sampling: https://github.com/samber/slog-sampling
[21]
slog-shim: https://github.com/sagikazarmark/slog-shim
[22]
sloggen: https://github.com/go-simpler/sloggen
[23]
sloglint: https://github.com/go-simpler/sloglint
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/uh0GJxvLVgyRq0J6u9c_2w