首頁 > 後端開發 > Golang > slog:Go官方的結構化日誌包開發的怎麼樣了?該如何使用?

slog:Go官方的結構化日誌包開發的怎麼樣了?該如何使用?

發布: 2023-08-04 17:01:02
轉載
1299 人瀏覽過

熟悉Go 的同學都知道Go 語言標準庫log 有許多痛點,例如沒有日誌分級、沒有結構化(沒有JSON 格式)、擴展性差等,為了解決這些問題Go 官方推出了結構化日誌包slog,目前這個庫正在開發階段,已經進入了實驗庫:golang.org/x/exp/slog,目前版本是v0.0.0。

這篇文章我們就來看下 slog 套件怎麼用?

安裝

使用下面的指令安裝:

go get golang.org/x/exp/slog
登入後複製

開箱即用

func main() {
 slog.Info("Go is best language!", "公众号", "Golang来啦")
}
登入後複製

輸出:

2023/01/23 10:23:37 INFO Go is best language! 公众号=Golang来啦
登入後複製

看輸出有點類似標準函式庫log 的輸出。 slog 函式庫裡一個很重要結構體就是 Logger,透過它就可以呼叫日誌記錄函數 Info()、Debug() 等。這個我們沒有創建 Logger,會使用預設的,大家可以點進去看下源碼。

Handler

Handler 定義成一個接口,這可以讓slog 的擴展性更強,slog 提供了兩個內建的Handler 實現:TextHandler 和JSONHandler,另外我們可以基於第三方log 套件定義或自己定義Handler 的實現,這個我們後面會講到。

type Handler interface {
 Enabled(Level) bool
 Handle(r Record) error
 WithAttrs(attrs []Attr) Handler
 WithGroup(name string) Handler
}
登入後複製

Text Handler

TextHandler 會像標準庫 log 套件一樣輸出日誌以一行文字。

func main() {
 textHandler := slog.NewTextHandler(os.Stdout)
 logger := slog.New(textHandler)
 logger.Info("Go is best language!", "公众号", "Golang来啦")
}
登入後複製

輸出:

time=2023-01-23T10:48:41.365+08:00 level=INFO msg="Go is best language!" 公众号=Golang来啦
登入後複製

我們看到,輸出的日誌以「key1=value1 key2=value2 … keyN=valueN」形式呈現。

JSON Handler

我們將上面的 NewTextHandler() 換成 NewJSONHandler()

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout)
 logger := slog.New(textHandler)
 logger.Info("Go is best language!", "公众号", "Golang来啦")
}
登入後複製

##輸出:

{"time":"2023-01-23T11:02:27.1606485+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦"}
登入後複製

从输出可以看到,日志已 json 格式记录,这样的结构化日志非常适合机器解析。

日志选项

日常开发中我们一般都会在日志里面记录在哪个文件哪一行记录了这条日志,这样有利于排查问题。或者,有时候需要更改日志级别,那这些该怎么实现呢?

如果我们翻看源码就能发现,上面提到的 TextHandler 和 JSONHandler 都使用默认的 HandlerOptions,它是一个结构体。

type HandlerOptions struct {
 AddSource bool
 Level Leveler
 ReplaceAttr func(groups []string, a Attr) Attr
}
登入後複製

通过 slog 的源代码注释可以看出,如果 AddSource 设置为 true,则记录日志时会以 ("source", "file:line") 的方式记录来源;Level 用于调整日志级别。

默认情况下,slog 只会记录 Info 及以上级别的日志,不会记录 Debug 级别的日志。

func main() {
 logger := slog.New(slog.NewJSONHandler(os.Stdout))
 logger.Debug("记录日志-debug",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
 logger.Info("记录日志-info",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
}
登入後複製

输出:

{"time":"2023-01-23T15:36:14.8610328+08:00","level":"INFO","msg":"记录日志-info","公众号":"Golang来啦","time":0}
登入後複製

这样的话,我们就可以自定义 option。

func main() {
 opt := slog.HandlerOptions{   // 自定义option
  AddSource: true,
  Level:     slog.LevelDebug,   // slog 默认日志级别是 info
 }

 logger := slog.New(opt.NewJSONHandler(os.Stdout))
 logger.Debug("记录日志-debug",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
 logger.Info("记录日志-info",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
}
登入後複製

输出:

{"time":"2023-01-23T15:38:45.3747228+08:00","level":"DEBUG","source":"D:/examples/context/demo1/demo1.go:81","msg":"记录日志-debug","公众号":"Golang来啦","time":0}
{"time":"2023-01-23T15:38:45.3949544+08:00","level":"INFO","source":"D:/examples/context/demo1/demo1.go:84","msg":"记录日志-info","公众号":"Golang来啦","time":0}
登入後複製

从输出可以看到记录日志的时候显示了来源,同时也记录了 debug 级别的日志。

SetDefault() 设置默认 Logger

有一点值得注意的是,slog.SetDefault() 会将传进来的 logger 作为默认的 Logger,所以下面这两行输出是一样的:

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout)
 logger := slog.New(textHandler)
 slog.SetDefault(logger)

 logger.Info("Go is best language!", "公众号", "Golang来啦")
 slog.Info("Go is best language!", "公众号", "Golang来啦")
}
登入後複製

输出:

{"time":"2023-01-23T11:17:32.7518696+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦"}
{"time":"2023-01-23T11:17:32.7732035+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦"}
登入後複製

另外,如果设置里默认的 Logger,调用 log 包方法时也会使用默认的:

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout)
 logger := slog.New(textHandler)
 slog.SetDefault(logger)

 log.Print("something went wrong")
 log.Fatalln("something went wrong")
}
登入後複製

输出:

{"time":"2023-01-23T11:18:31.5850509+08:00","level":"INFO","msg":"something went wrong"}
{"time":"2023-01-23T11:18:31.6043829+08:00","level":"INFO","msg":"something went wrong"}
exit status 1
登入後複製

两种记录日志的方式

通过 slog 包记录日志除了上面提到的这种方式:

logger.Info("Go is best language!", "公众号", "Golang来啦")
登入後複製

这种方式会涉及到额外的内存分配,主要是为了简介设计的。

另外一种记录日志方式就像下面这样:

logger.LogAttrs(slog.LevelInfo, "Go is best language!", slog.String("公众号", "Golang来啦"))
登入後複製

这两种输出日志格式都是一样的,第二种为了提高记录日志的性能而设计的,需要自己指定日志级别、参数属性(以键值对的方式指定)。

目前 slog 包支持下面这些属性:

String
Int64
Int
Uint64
Float64
Bool
Time
Duration
登入後複製

我们还可以多指定一些属性:

logger.LogAttrs(slog.LevelInfo, "Go is best language!", slog.String("公众号", "Golang来啦"), slog.Int("age", 18))
登入後複製

输出:

{"time":"2023-01-23T11:45:11.7921124+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦","age":18}
登入後複製

如何绑定一组属性

学到这里我就在想,假如我想在一个 key 下面绑定一组 key-value 值该怎么做呢?这种需求在日常开发中是很常见的,我翻了翻源码,slog 还真的提供了相关方法 -- slog.Group()。

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout)
 logger := slog.New(textHandler)
 slog.SetDefault(logger)

 logger.Info("Usage Statistics",
  slog.Group("memory",
   slog.Int("current", 50),
   slog.Int("min", 20),
   slog.Int("max", 80)),
  slog.Int("cpu", 10),
  slog.String("app-version", "v0.0.0"),
 )
}
登入後複製

输出:

{"time":"2023-01-23T13:45:26.9179901+08:00","level":"INFO","msg":"Usage Statistics","memory":{"current":50,"min":20,"max":80},"cpu":10,"app-version":"v0.0.0"}
登入後複製

memory 元素下面对应不同的 key-value。

如何绑定公共的属性

日常开发中,可能会遇到每一条日志需要记录一些相同的公共信息,比如 app-version。

...

logger.Info("Usage Statistics",
  slog.Group("memory",
   slog.Int("current", 50),
   slog.Int("min", 20),
   slog.Int("max", 80)),
  slog.Int("cpu", 10),
  slog.String("app-version", "v0.0.0"),
 )
 logger.Info("记录日志",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()), slog.String("app-version", "v0.0.0"))

...
登入後複製

如果想上面这样,每次都记录一次 app-version 的话就有点繁琐了。好在 slog 自带的 TextHandler 和 JSONHandler 提供了 WithAttrs() 方法可以实现绑定公共属性。

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout).WithAttrs([]slog.Attr{slog.String("app-version", "v0.0.0")})
 logger := slog.New(textHandler)
 slog.SetDefault(logger)

 logger.Info("Usage Statistics",
  slog.Group("memory",
   slog.Int("current", 50),
   slog.Int("min", 20),
   slog.Int("max", 80)),
  slog.Int("cpu", 10),
 )
 logger.Info("记录日志",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
}
登入後複製

输出:

{"time":"2023-01-23T14:01:46.2845325+08:00","level":"INFO","msg":"Usage Statistics","app-version":"v0.0.0","memory":{"current":50,"min":20,"max":80},"cpu":10}
{"time":"2023-01-23T14:01:46.303597+08:00","level":"INFO","msg":"记录日志","app-version":"v0.0.0","公众号":"Golang来啦","time":0}
登入後複製

从输出可以看到两条日志都记录了 app-version,这种记录方式就简洁多了。

通过 context 存储或提取 Logger

slog 的 Logger 还与 context.Context 结合在一起,比如通过 slog.WithContext() 存储 Logger、通过 slog.FromContext() 提取 Logger。这样我们就可以在不同函数之间通过 context 传递 Logger。

func main() {
 logger := slog.New(slog.NewJSONHandler(os.Stdout))
 http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
  l := logger.With("path", r.URL.Path).With("user-agent", r.UserAgent()) // With() 绑定额外的信息

  ctx := slog.NewContext(r.Context(), l) // 生成 context

  handleRequest(w, r.WithContext(ctx))
 })

 http.ListenAndServe(":8080", nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
 logger := slog.FromContext(r.Context()) // 提取 Logger

 logger.Info("handling request",
  "status", http.StatusOK)

 w.Write([]byte("Hello World"))
}
登入後複製

执行程序并访问地址: http://127.0.0.1:8080/hello

输出:

{"time":"2023-01-23T14:36:26.6303067+08:00","level":"INFO","msg":"handling request","path":"/hello","user-agent":"curl/7.83.1","status":200}
登入後複製

上面这种使用 Logger 的方式是不是还挺方便的,不过很遗憾的是,在最新的 slog 包里,这两个方法已经被作者移除掉了。

slog:Go官方的結構化日誌包開發的怎麼樣了?該如何使用?

我很好奇作者为什么把这两个方法移除掉,后面翻到 slog 提案[1] 下面作者留言[2],大意是说这种使用方式有比较大的争议(主要是函数之间能否使用 context),而且如果使用者喜欢这种使用方式的话,也可以自己实现,所以把这两个方法移除了。

如果需要自己实现通过 context 储存和提取 Logger,你知道怎么实现吗?欢迎留言区交流,嘻嘻。

如何集成第三方日志包

在讲 Handler 那一节时提到过,如果我们实现了 Handler 接口,就可以将第三方 log 与 Logger 集成,那该怎么实现呢?我们就拿 logrus 日志包举例吧。

package main

import (
 "fmt"
 "github.com/sirupsen/logrus"
 "golang.org/x/exp/slog"
 "net"
 "net/http"
 "os"
)

func init() {
 // 设置logrus
 logrus.SetFormatter(&logrus.JSONFormatter{})
 logrus.SetOutput(os.Stdout)
 logrus.SetLevel(logrus.DebugLevel)
}

func main() {
 // 将 Logrus 与 Logger 集成在一块
 logger := slog.New(&LogrusHandler{
  logger: logrus.StandardLogger(),
 })

 logger.Error("something went wrong", net.ErrClosed,
  "status", http.StatusInternalServerError)
}

type LogrusHandler struct {
 logger *logrus.Logger
}

func (h *LogrusHandler) Enabled(_ slog.Level) bool {
 return true
}

func (h *LogrusHandler) Handle(rec slog.Record) error {
 fields := make(map[string]interface{}, rec.NumAttrs())

 rec.Attrs(func(a slog.Attr) {
  fields[a.Key] = a.Value.Any()
 })

 entry := h.logger.WithFields(fields)

 switch rec.Level {
 case slog.LevelDebug:
  entry.Debug(rec.Message)
 case slog.LevelInfo:
  entry.Info(rec.Message)
 case slog.LevelWarn:
  entry.Warn(rec.Message)
 case slog.LevelError:
  entry.Error(rec.Message)
 }

 fmt.Println("测试是否走了这个方法:记录日志")

 return nil
}

func (h *LogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
 // 为了演示,此方法就没有实现,但不影响效果
 return h
}

func (h *LogrusHandler) WithGroup(name string) slog.Handler {
 // 为了演示,此方法就没有实现,但不影响效果
 return h
}
登入後複製

输出:

{"err":"use of closed network connection","level":"error","msg":"something went wrong","status":500,"time":"2023-01-23T16:07:40+08:00"}
测试是否走了这个方法:记录日志
登入後複製

追查代码发现,通过调用 slog 的方法记录日志时都会调用 logPC() 方法生成一条 Record,最终会交给 Handler 接口的具体实现方法 Handle(),这里就是我们自己实现的方法

func (h *LogrusHandler) Handle(rec slog.Record) error {}
登入後複製

从输出就可以看出,最终调用了自己实现的 Handle() 方法,走的是 logrus 包的方法 entry.Error()。

总结

这篇文章主要介绍了 slog 包的一些主要方法的使用,简单说了下里面一些函数、方法的实现,更详细的细节大家可以自行查看源码。目前中文社区关于 slog 的文章不多(可能是我没发现,欢迎补充),我发现比较好的已经在底部的参考文章里列出来了,作为补充可以深入了解 slog 包。另外感兴趣的同学可以看下关于 slog 的提案(里面会实时更新一些信息以及社区开发者的讨论)和 slog 包的设计文档,具体链接看参考文章。欢迎留言交流,一起学习成长。

以上是slog:Go官方的結構化日誌包開發的怎麼樣了?該如何使用?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
go
來源:Golang菜鸟
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板