原文链接: https://tangx.in/posts/2023/01/06/how-to-set-debug-level-in-golang-slog/

在 golang 中, 日志统一 一直都是一个头疼的问题。

在 exp 中, Go 加入了一个新库 `exp/slog`[1], 希望能转正。

slog
sloglog
func main() {
 slog.Debug("debug")
 slog.Info("info")
 slog.Warn("warn")
 slog.Error("err", fmt.Errorf("game over"))
 // slog.Fatal("don't support")
}

// 2023/01/06 07:41:50 INFO info
// 2023/01/06 07:41:50 WARN warn
// 2023/01/06 07:41:50 ERROR err err="game over"
Debugslog.SetLevel(level)
sloginfoDEBUGhandlerslogFatalslog

追踪源码, 看实现

不要着急, 先来看看源码。

Debug
func main() {
 slog.Debug("debug")
}
default Logger
// Debug calls Logger.Debug on the default logger.
func Debug(msg string, args ...any) {
 Default().LogDepth(1, LevelDebug, msg, args...)
}
LogDepthLevelDebugiffalse
func (l *Logger) LogDepth(calldepth int, level Level, msg string, args ...any) {
 if !l.Enabled(level) {
  return
 }
 var pcs [1]uintptr
 runtime.Callers(calldepth+2, pcs[:])
 l.logPC(nil, pcs[0], level, msg, args...)
}
Default()slog.Logger
var defaultLogger atomic.Value

func init() {
 defaultLogger.Store(New(newDefaultHandler(log.Output)))
}

// Default returns the default Logger.
func Default() *Logger { return defaultLogger.Load().(*Logger) }
defaultLogger*Logger
defaultLoggerinitnewDefaultHandler()slog.HandlerNew()*slog.LoggerStore()*slog.LoggerDefault()*slog.Logger
newDefaultHandler
func newDefaultHandler(output func(int, string) error) *defaultHandler {
 return &defaultHandler{
  ch:     &commonHandler{json: false},
  output: output,
 }
}

func (*defaultHandler) Enabled(l Level) bool {
 return l >= LevelInfo
}
newDefaultHandlerdefaultHandler
EnabledInfo
slog.Debug

slog 的两大模块

Debug
Debugslog.Handler
slog
slog.LoggerDebug, Info
// To create a new Logger, call [New] or a Logger method
// that begins "With".
type Logger struct {
 handler Handler // for structured logging
 ctx     context.Context
}

// New creates a new Logger with the given non-nil Handler and a nil context.
func New(h Handler) *Logger {
 if h == nil {
  panic("nil Handler")
 }
 return &Logger{handler: h}
}
slog.Handler
// Any of the Handler's methods may be called concurrently with itself
// or with other methods. It is the responsibility of the Handler to
// manage this concurrency.
type Handler interface {
 // Enabled reports whether the handler handles records at the given level.
 // The handler ignores records whose level is lower.
 // Enabled is called early, before any arguments are processed,
 // to save effort if the log event should be discarded.
 Enabled(Level) bool
 Handle(r Record) error
 WithAttrs(attrs []Attr) Handler
 WithGroup(name string) Handler
}

如何实现 Debug 输出

slog
debugHandlerEnableddebugnewDebugLoggernewDebugLoggerslog Default Hanlder
// 定义了自己的 debugHanlder
type debugHandler struct {
 slog.Handler
}

// newDebugLogger 使用 slog Default Handler 作为实际载体
func newDebugLogger() *slog.Logger {
 dh := &debugHandler{
  // handler 使用 slog 默认的 Handler
  slog.Default().Handler(),
 }

 return slog.New(dh)
}

// Enabled 重写了默认的日志登记的判断方法,支持 Debug 日志
func (dh *debugHandler) Enabled(l slog.Level) bool {
 return l >= slog.LevelDebug
}
func main() {

 // 创建 debugLogger 对象
 log := newDebugLogger()

 log.Debug("debug")
 log.Info("info")
 log.Warn("warn")
 log.Error("err", fmt.Errorf("game over"))
 // log.Fatal("don't support")
}

// 2023/01/06 09:15:18 DEBUG debug
// 2023/01/06 09:15:18 INFO info
// 2023/01/06 09:15:18 WARN warn
// 2023/01/06 09:15:18 ERROR err err="game over"

控制日志等级

很明显, 上面的例子也有一个问题, 不能 自定义日志等级, 所有日志都会打印出来。

slog 官方给了一个 slog 实现自定义日志等级 -  Go Playground[3] 的 Demo。

比我们之前的代码多很多, 也不是很复杂。

LevelHandlerslog.Handlerslog.New()LevelLoggerwarn
package main

import (
 "os"

 "golang.org/x/exp/slog"
)

// A LevelHandler wraps a Handler with an Enabled method
// that returns false for levels below a minimum.
type LevelHandler struct {
 level   slog.Leveler
 handler slog.Handler
}

// NewLevelHandler returns a LevelHandler with the given level.
// All methods except Enabled delegate to h.
func NewLevelHandler(level slog.Leveler, h slog.Handler) *LevelHandler {
 // Optimization: avoid chains of LevelHandlers.
 if lh, ok := h.(*LevelHandler); ok {
  h = lh.Handler()
 }
 return &LevelHandler{level, h}
}

// Enabled implements Handler.Enabled by reporting whether
// level is at least as large as h's level.
func (h *LevelHandler) Enabled(level slog.Level) bool {
 return level >= h.level.Level()
}

// Handle implements Handler.Handle.
func (h *LevelHandler) Handle(r slog.Record) error {
 return h.handler.Handle(r)
}

// WithAttrs implements Handler.WithAttrs.
func (h *LevelHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
 return NewLevelHandler(h.level, h.handler.WithAttrs(attrs))
}

// WithGroup implements Handler.WithGroup.
func (h *LevelHandler) WithGroup(name string) slog.Handler {
 return NewLevelHandler(h.level, h.handler.WithGroup(name))
}

// Handler returns the Handler wrapped by h.
func (h *LevelHandler) Handler() slog.Handler {
 return h.handler
}

func main() {
 th := slog.HandlerOptions{
  // Remove time from the output.
  ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
   if a.Key == slog.TimeKey {
    return slog.Attr{}
   }
   return a
  },
 }.NewTextHandler(os.Stdout)

 logger := slog.New(NewLevelHandler(slog.LevelWarn, th))
 logger.Info("not printed")
 logger.Warn("printed")

}

// level=WARN msg=printed
go-jarvis/logr

开源了一个自己实现的 go-jarvis/logr - Github[4]

log.Debug(format, ...any)ValuerStart / Stop
func TestDefault(t *testing.T) {

 log := Default().SetLevel(DebugLevel)
 err := errors.New("New_ERROR")

 log = log.With(
  "kk", "vv",
  "caller", CallerFile(4, false),
  "gg",
 )

 log.Debug("number=%d", 1)
 log.Info("number=%d", 1)
 log.Warn(err)
 log.Error(err)

 ctx := WithLogger(context.Background(), log)
 subcaller(ctx)
}


func subcaller(ctx context.Context) {
 log := FromContext(ctx)

 log = log.Start() // time cost
 defer log.Stop()

 time.Sleep(532 * time.Millisecond)
 log.Info("account=%d", 100)
}

// 2023/01/06 10:31:53 DEBUG number=1 kk=vv caller=logr_test.go:21#TestDefault gg=LACK_Unknown
// 2023/01/06 10:31:53 INFO number=1 kk=vv caller=logr_test.go:22#TestDefault gg=LACK_Unknown
// 2023/01/06 10:31:53 WARN New_ERROR kk=vv caller=logr_test.go:23#TestDefault gg=LACK_Unknown
// 2023/01/06 10:31:53 ERROR New_ERROR kk=vv caller=logr_test.go:24#TestDefault gg=LACK_Unknown
// 2023/01/06 10:31:53 INFO account=100 kk=vv caller=logr_test.go:37#subcaller gg=LACK_Unknown
// 2023/01/06 10:31:53 INFO time-cost kk=vv caller=logr.go:101#Stop gg=LACK_Unknown cost=533ms cost_caller=logr_test.go:38#subcaller

参考资料

[1]

exp/slog

[2]

Golang 库: 为什么 Golang slog 库不支持 slog.Fatal API: https://tangx.in/posts/2023/01/06/why-dont-golang-slog-support-fatal-api/

[3]

slog 实现自定义日志等级 -  Go Playground: https://go.dev/play/p/WXrfqyfKGUt

[4]

go-jarvis/logr - Github: https://github.com/go-jarvis/logr

[5]

go-kratos/kratos - Github: https://github.com/go-kratos/kratos