Golang 库: 怎么使用 golang slog 设置日志 Debug 等级

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

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

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

这里有一些关于 slog 的介绍, 可以参考 Go 十年了,终于想起要统一 log 库了!

使用 slog 习惯误区, 默认日志级别是 Info

如果直接把 slog 当成 log 使用, 可能又一点问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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"

可以看到 Debug 无法打印, 找了一圈, 还没有类似 slog.SetLevel(level) 方法实现等级设置。

  1. slog 默认日志级别info, 无法输出 DEBUG 日志。
    • 需要自定义 handler 实现日志级别判断。 后文详细说。
  2. slog 默认不支持 Fatal API。

追踪源码, 看实现

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

1. 跟踪 Debug, 查看是如何打印日志的

1
2
3
func main() {
	slog.Debug("debug")
}

2. 查看 default Logger 是怎么实现的。

1
2
3
4
// Debug calls Logger.Debug on the default logger.
func Debug(msg string, args ...any) {
	Default().LogDepth(1, LevelDebug, msg, args...)
}

先跟踪 LogDepth 可以看到, Debug API 是传入了日志级别 LevelDebug, 并且进行了 日志等级 比较。 之所以无法打印日志, 应该是在 if 条件语句判断为 false 跳过了。

1
2
3
4
5
6
7
8
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...)
}

3. 退出来, 跟踪 Default() 是怎么实现 slog.Logger 的。

1
2
3
4
5
6
7
8
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

  1. defaultLogger 是一个 原子 类型。
  2. init 函数中
    1. 通过 newDefaultHandler() 创建了一个 slog.Handler
    2. 通过 New() 创建了一个 *slog.Logger, 这个就是打印日志的 主体
    3. 通过 Store()*slog.Logger 保存起来
  3. 通过 Default()内容 读取出来, 并断言成 *slog.Logger, 用于打印日志。

4. 跟踪 newDefaultHandler, 查看怎么实现的日志级别判断。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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
}

可以看到 newDefaultHandler 返回了一个 defaultHandler 结构体对象。

Enabled 方法中, 要求 日志级别必须要 大于等于 Info

这就是为什么 slog.Debug 无法正常输出的原因。

slog 的两大 模块

那是不是 slog 就无法实现 Debug 了呢? 用脚指头想都不能这么认为。

要实现 Debug 日志, 需要自己实现 slog.Handler

个人认为, slog 两大模块

  1. slog.Logger: 负责对外提供 标准的 API 接口。 类似 Debug, Info 等等。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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}
}
  1. slog.Handler: 对内负责实际生产工作, 诸如 等级判断、 输出格式、 数据处理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 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 默认是无法实现 Debug 日志输出了。 但是我们可以通过 自定义Handler 实现。

  1. 首先, 创建 debugHandler 对象, 用于处理实际日志业务。
  2. 其次, 重写 Enabled 方法, 实现日志等级大于 debug 的输出
  3. 最后, 通过 newDebugLogger 函数, 创建 自定义的 Logger 对象。
    1. newDebugLogger 中, 是用 slog Default Hanlder 作为最底层的操作对象。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 定义了自己的 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
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 的 Demo。

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

  1. 定义 LevelHandler, 支持 level 字段
  2. 重写了所有 slog.Handler 应该的具有的接口方法
  3. 使用 slog.New() 创建自定义的 LevelLogger
  4. 日记级别定义为 warn, 并测试。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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

  1. 实现了日志等级
  2. 延续了 log.Debug(format, ...any) 的使用习惯
  3. 参考了 go-kratos/kratos - Github 的日志库
    • 采用 Valuer 接口, 实现了日志数据 暂存与延迟计算
  4. 支持 Start / Stop 方法, 计算函数执行时间。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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