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) 方法实现等级设置。
- slog默认日志级别 是- info, 无法输出- DEBUG日志。- 需要自定义 handler实现日志级别判断。 后文详细说。
 
- slog默认不支持- FatalAPI。
追踪源码, 看实现
不要着急, 先来看看源码。
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
- defaultLogger是一个 原子 类型。
- 在 init函数中- 通过 newDefaultHandler()创建了一个slog.Handler
- 通过 New()创建了一个*slog.Logger, 这个就是打印日志的 主体
- 通过 Store()将*slog.Logger保存起来
 
- 通过 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 两大模块
- 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}
}
 | 
- 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 实现。
- 首先, 创建 debugHandler对象, 用于处理实际日志业务。
- 其次, 重写 Enabled方法, 实现日志等级大于debug的输出
- 最后, 通过 newDebugLogger函数, 创建 自定义的 Logger 对象。- 在 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。
比我们之前的代码多很多, 也不是很复杂。
- 定义 LevelHandler, 支持 level 字段
- 重写了所有 slog.Handler应该的具有的接口方法
- 使用 slog.New()创建自定义的LevelLogger
- 日记级别定义为 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
- 实现了日志等级
- 延续了 log.Debug(format, ...any)的使用习惯
- 参考了
go-kratos/kratos - Github
的日志库- 采用 Valuer接口, 实现了日志数据 暂存与延迟计算
 
- 支持 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
 |