原文链接: https://tangx.in/posts/2023/01/06/how-to-set-debug-level-in-golang-slog/
在 golang 中, 日志统一 一直都是一个头疼的问题。
在 exp 中, Go 加入了一个新库 `exp/slog`[1], 希望能转正。
slog
习惯误区, 默认日志级别是 Info如果直接把 slog
当成 log
使用, 可能又一点问题。
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。slog
终止进程了。参考 Golang 库: 为什么 Golang slog 库不支持 `slog.Fatal` API[2]不要着急, 先来看看源码。
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...)
}
先跟踪 LogDepth
可以看到, Debug API 是传入了日志级别 LevelDebug
, 并且进行了 日志等级 比较。之所以无法打印日志, 应该是在 if
条件语句判断为 false
跳过了。
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
defaultLogger
是一个 原子 类型。init
函数中newDefaultHandler()
创建了一个 slog.Handler
New()
创建了一个 *slog.Logger
, 这个就是打印日志的 主体Store()
将 *slog.Logger
保存起来Default()
将 内容 读取出来, 并断言成 *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
}
可以看到 newDefaultHandler
返回了一个 defaultHandler
结构体对象。
在 Enabled
方法中, 要求 日志级别必须要 大于等于 Info
。
这就是为什么 slog.Debug
无法正常输出的原因。
那是不是 slog 就无法实现 Debug
了呢?用脚指头想都不能这么认为。
要实现 Debug
日志, 需要自己实现 slog.Handler
。
个人认为, slog
两大模块
slog.Logger
: 负责对外提供 标准的 API 接口。类似 Debug, 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
}
slog
默认是无法实现 Debug 日志输出了。但是我们可以通过 自定义Handler 实现。
debugHandler
对象, 用于处理实际日志业务。Enabled
方法, 实现日志等级大于 debug
的输出newDebugLogger
函数, 创建 自定义的 Logger 对象。newDebugLogger
中, 是用 slog 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。
比我们之前的代码多很多, 也不是很复杂。
LevelHandler
, 支持 level 字段slog.Handler
应该的具有的接口方法slog.New()
创建自定义的 LevelLogger
warn
, 并测试。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)
的使用习惯Valuer
接口, 实现了日志数据 暂存与延迟计算Start / 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
: https://golang.org/x/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