前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang 库: golang slog 怎么设置日志 Debug 等级

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

作者头像
老麦
发布2023-02-25 19:51:50
2.3K0
发布2023-02-25 19:51:50
举报
文章被收录于专栏:Go与云原生Go与云原生

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

原文链接: 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 使用, 可能又一点问题。

代码语言:javascript
复制
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。
    • 换而言之, 无法使用 slog 终止进程了。参考 Golang 库: 为什么 Golang slog 库不支持 `slog.Fatal` API[2]

追踪源码, 看实现

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

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

代码语言:javascript
复制
func main() {
 slog.Debug("debug")
}

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

代码语言:javascript
复制
// 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 跳过了。

代码语言:javascript
复制
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 的。

代码语言:javascript
复制
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, 查看怎么实现的日志级别判断。

代码语言:javascript
复制
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 等等。
代码语言:javascript
复制
// 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: 对内负责实际生产工作, 诸如 等级判断、 输出格式、 数据处理
代码语言:javascript
复制
// 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 作为最底层的操作对象。
代码语言:javascript
复制
// 定义了自己的 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
}
代码语言:javascript
复制
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。

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

  1. 定义 LevelHandler, 支持 level 字段
  2. 重写了所有 slog.Handler 应该的具有的接口方法
  3. 使用 slog.New() 创建自定义的 LevelLogger
  4. 日记级别定义为 warn, 并测试。
代码语言:javascript
复制
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]

  1. 实现了日志等级
  2. 延续了 log.Debug(format, ...any) 的使用习惯
  3. 参考了 go-kratos/kratos - Github[5] 的日志库
    • 采用 Valuer 接口, 实现了日志数据 暂存与延迟计算
  4. 支持 Start / Stop 方法, 计算函数执行时间。
代码语言:javascript
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 熊猫云原生Go 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Golang 库: 怎么使用 golang slog 设置日志 Debug 等级
    • 使用 slog 习惯误区, 默认日志级别是 Info
      • 追踪源码, 看实现
        • 1. 跟踪 Debug, 查看是如何打印日志的
        • 2. 查看 default Logger 是怎么实现的。
        • 3. 退出来, 跟踪 Default() 是怎么实现 slog.Logger 的。
        • 4. 跟踪 newDefaultHandler, 查看怎么实现的日志级别判断。
      • slog 的两大 模块
        • 如何实现 Debug 输出
          • 控制日志等级
            • 自己实现的一个 go-jarvis/logr
              • 参考资料
          相关产品与服务
          日志服务
          日志服务(Cloud Log Service,CLS)是腾讯云提供的一站式日志服务平台,提供了从日志采集、日志存储到日志检索,图表分析、监控告警、日志投递等多项服务,协助用户通过日志来解决业务运维、服务监控、日志审计等场景问题。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档