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
默认不支持 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
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
|