探索 Go slog 標(biāo)準(zhǔn)庫:設(shè)計與應(yīng)用
在 Go1.21[2] 中引入的 log/slog[3] 軟件包試圖彌補(bǔ)原有日志軟件包的不足,即日志缺乏結(jié)構(gòu)化和級別特性。正如提案[4]中提到的,log/slog 包旨在創(chuàng)建一個零依賴、易用、高性能、靈活高效的日志系統(tǒng)。如果你對它的概念感興趣,并想更好的利用它,那就請跟隨我深入了解它的設(shè)計、實(shí)現(xiàn)和應(yīng)用。

slog設(shè)計
為實(shí)現(xiàn)其目的,slog 的設(shè)計具有高度靈活性。它支持結(jié)構(gòu)化日志,能以 JSON 或其他格式輸出日志,以便后續(xù)分析和處理。出色的模塊化設(shè)計將日志功能分為多個組件,如日志級別管理、輸出格式化、日志傳輸?shù)?,每個組件都可以獨(dú)立配置和替換。其異步處理支持機(jī)制可確保日志操作不會阻礙主應(yīng)用程序的執(zhí)行。
// 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負(fù)責(zé)所有日志打印操作。Handler定義日志輸出格式,默認(rèn)實(shí)現(xiàn)為 TextHandler 和 JSONHandler。Record定義日志的詳細(xì)信息,如時間、級別等。這些接口不僅能以多種方式擴(kuò)展 slog(如添加新的處理程序),還能滿足不同日志框架的需求和配置。
slog用法
slog 的基本用法與 zap 或 Golang 的其他第三方日志框架類似。
1.日志打印
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=123slog 為不同級別的日志提供了不同的打印方式和相應(yīng)的參數(shù)類型轉(zhuǎn)換。
2.Handler
通過下面的代碼,我們可以將默認(rèn)打印方式修改為內(nèi)置的 TextHandler 或 JSONHandler。
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// logger.Info/Debug/Error請看輸出結(jié)果。
// 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] 方法創(chuàng)建 Logger 來替換 slog 中的默認(rèn) Logger,默認(rèn) 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}3.消息處理
對 Record 和 attr 的處理是 slog 核心功能的一部分。每條日志記錄都由時間、級別、消息等參數(shù)和一組鍵值對組成。處理這些參數(shù)的 API 提供了靈活性。使用 With 方法,用戶可以輕松添加固定屬性,這些屬性將出現(xiàn)在該日志記錄器生成的每條日志記錄中。通過 Group 方法,用戶可以匯總多個屬性,進(jìn)行統(tǒng)一處理。請看下面的示例。
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 中處理參數(shù)的主要 API,例如上例中的 slog.String 和 slog.Any,以及 slog.Bool[7]、slog.Int[8]、slog.Float64[9]、slog.Duration[10]、slog.Time[11]等。
Level 通過實(shí)現(xiàn) Leveler[12] 接口的 Level() 為 slog 提供默認(rèn)日志級別設(shè)置。
const (
 LevelDebug Level = -4
 LevelInfo  Level = 0
 LevelWarn  Level = 4
 LevelError Level = 8
)
func (l Level) Level() Level { return l }日志應(yīng)用本地機(jī)器的默認(rèn)時區(qū)設(shè)置,但也可以使用 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), 
 )不過,如果我們需要在不同于本地機(jī)器的時區(qū)打印日志,就需要自定義處理程序,因為 slog 軟件包缺少用于全局修改的默認(rèn) API。
4.上下文處理
可以通過兩種方式將上下文添加到日志記錄中。
- 通過在日志處理中加入 context.Context,可以在函數(shù)和goroutine之間傳遞上下文信息。
 - Logger 提供了帶context的日志打印方法,如 DebugContext[13]、InfoContext[14]、WarnContext[15] 和 ErrorContext[16]。context.Context 被添加到之前的方法參數(shù)列表中,并將 ctx 放在首位,這符合上下文的使用原則。
 
func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any)但如果直接使用這些方法將context中的參數(shù)添加到日志中,就會像我一樣感到困惑,因為這樣做是行不通的。例如,在以下代碼中,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閱讀源代碼后,你會驚訝的發(fā)現(xiàn),默認(rèn) defaultHandler 只打印消息和參數(shù)列表,卻不處理傳入的 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 中正式發(fā)布的 slog 只提供了相關(guān) API 而沒有提供實(shí)現(xiàn),因此需要定制處理程序來實(shí)現(xiàn)相關(guān)功能。
可通過父日志記錄器構(gòu)建具有層次關(guān)系的日志記錄器,這些日志記錄器繼承并擴(kuò)展了基本日志記錄功能
例如,在前面的With和Group示例中,在打印 Info 和 Error 日志的過程中重復(fù)傳遞了服務(wù)和版本信息,而日志程序的繼承功能可以簡化這一過程。
// 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 的設(shè)計非常靈活,便于定制日志。
自定義日志級別
與官方 LogLevel 一樣,可以通過實(shí)現(xiàn) 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跟蹤級別日志在未修改相應(yīng)HandlerOptions時是隱藏的,可通過修改默認(rèn)日志級別打開。
slog.SetLogLoggerLevel(LevelTrace)自定義HandlerOptions HandlerOptions[17]提供三種修改日志的方法。
- 顯示源代碼。使用 AddSource:true,可以在日志中添加文件、行號和方法名稱等信息。
 - 修改日志級別。LevelTrace 的作用與上述 SetLogLoggerLevel 相同。
 - 定義 ReplaceAttr 可調(diào)用函數(shù),用于修改日志中的 Attr 鍵值對。
 
對于缺少默認(rèn)格式化支持的錯誤,可以通過實(shí)現(xiàn) ReplaceAttr 來加以改進(jìn)。
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 自定義日志的最后一種方法是實(shí)現(xiàn)自己的Handler。下面的 ContextHandler 提供了將上下文中的參數(shù)整合到日志中的功能。
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 支持通過延遲計算優(yōu)化參數(shù)處理,只在實(shí)際需要日志信息時才執(zhí)行參數(shù)的高性能字符串操作,以減少不必要的性能開銷。
在默認(rèn)日志方法中,只打印默認(rèn)級別以上的日志。Attrs 將被添加到打印前創(chuàng)建的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 利用內(nèi)置 buffer.go[18] 中的對象池技術(shù)重復(fù)使用日志條目對象,減少了內(nèi)存應(yīng)用和釋放以及垃圾回收的頻率,從而提高了應(yīng)用性能。
slog實(shí)踐
到目前為止,我們已經(jīng)做好了在項目中使用slog的準(zhǔn)備,但要牢記以下幾點(diǎn)。
(1) 對敏感數(shù)據(jù)脫敏
數(shù)據(jù)安全在日志處理中至關(guān)重要。在將對象對象打印到日志時,應(yīng)及時屏蔽或模糊敏感信息,以避免數(shù)據(jù)泄漏,這可以通過相應(yīng)結(jié)構(gòu)體中的 LogValue 接口來實(shí)現(xiàn)。下面我們來看一個包含電子郵件和密碼的用戶類型示例。
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"}(2) 將slog級別提取為配置項,并為不同環(huán)境配置不同級別
項目通常部署在多個環(huán)境中,如 dev、staging、prod 等。通過將日志級別提取為環(huán)境參數(shù),并通過 Kubernetes 的 configMap 注入,可以在不同環(huán)境中應(yīng)用不同日志級別,從而減少開銷。
(3) 處理上下文信息
上例中的上下文實(shí)現(xiàn)是一個明智的選擇,可以通過優(yōu)化來動態(tài)加載上下文參數(shù)列表。在我們實(shí)現(xiàn)的代碼及許多第三方日志框架中,Context 被廣泛用于傳遞上下文信息,從而簡化了從第三方庫升級到 slog 的過程。
(4) 正確配置日志輸出目的地
根據(jù)需要配置日志輸出,如輸出到控制臺、文件、網(wǎng)絡(luò)服務(wù)等,或輸出到多個輸出目的地,以確保日志的可靠存儲。
通過自定義處理程序的寫入器,可以輕松修改日志輸出地址。
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社區(qū)有許多為 slog 定制的升級框架,以下是一些常用框架:
- slog-multi[19]:處理器鏈,如流水線、路由器、扇出等。
 - slog-sampling[20]:丟棄重復(fù)的日志條目。
 - slog-shim[21] 為 1.21 以下的 Go 版本提供向后兼容的 slog 支持。
 - sloggen[22] 生成各種輔助工具。
 - sloglint[23] 可確保代碼的一致性。
 
結(jié)論
slog 軟件包為管理應(yīng)用程序日志提供了強(qiáng)大而靈活的解決方案。其設(shè)計確保了高性能和多功能性,使開發(fā)人員能夠自定義日志級別、Handler配置以及Handler本身。使用 slog 不僅能加強(qiáng)消息處理和上下文信息集成,還能簡化日志記錄流程。為了有效實(shí)施,仔細(xì)考慮自定義日志級別和Handler以滿足特定項目要求至關(guān)重要。通過明智利用 slog 的功能,開發(fā)人員可以顯著提高日志實(shí)踐的效率和清晰度,確保代碼具有更好的可維護(hù)性和可讀性。
參考資料:
- [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
 















 
 
 















 
 
 
 