Rumah > pembangunan bahagian belakang > Golang > slog: Bagaimanakah perkembangan pakej log berstruktur rasmi Go berjalan? Bagaimana cara menggunakannya?

slog: Bagaimanakah perkembangan pakej log berstruktur rasmi Go berjalan? Bagaimana cara menggunakannya?

Lepaskan: 2023-08-04 17:01:02
ke hadapan
1325 orang telah melayarinya

Pelajar yang biasa dengan Go tahu bahawa log perpustakaan standard bahasa Go mempunyai banyak titik kesakitan, seperti tiada klasifikasi log, tiada struktur (tiada format JSON), skalabiliti yang lemah, dll. Untuk menyelesaikan masalah ini, Go melancarkan slog pakej log berstruktur secara rasmi, perpustakaan ini sedang dibangunkan dan telah memasuki perpustakaan percubaan: golang.org/x/exp/slog Versi semasa ialah v0.0.0.

Dalam artikel ini, mari kita lihat cara menggunakan pakej slog? . daripada log perpustakaan standard. Struktur yang sangat penting dalam perpustakaan slog ialah Logger, yang melaluinya fungsi pengelogan Info(), Debug(), dll. boleh dipanggil. Kami belum mencipta Logger untuk ini dan akan menggunakan yang lalai Anda boleh klik masuk untuk melihat kod sumber.

Handler

Handler ditakrifkan sebagai antara muka, yang boleh menjadikan slog lebih berskala menyediakan dua pelaksanaan Handler terbina dalam: TextHandler dan JSONHandler Selain itu, kami boleh mentakrifkannya berdasarkan log pihak ketiga pakej atau diri kita sendiri Tentukan pelaksanaan Handler, yang akan kita bincangkan kemudian.

go get golang.org/x/exp/slog
Salin selepas log masuk

Pengendali Teks

TextHandler akan mengeluarkan log sebagai baris teks sama seperti pakej log perpustakaan standard.

func main() {
 slog.Info("Go is best language!", "公众号", "Golang来啦")
}
Salin selepas log masuk

Output:

2023/01/23 10:23:37 INFO Go is best language! 公众号=Golang来啦
Salin selepas log masuk

Kami melihat bahawa log keluaran dibentangkan dalam bentuk "key1=value1 key2=value2 ... keyN=valueN". JSON Handler

Kami menggantikan NewTextHandler() di atas dengan NewJSONHandler()
type Handler interface {
 Enabled(Level) bool
 Handle(r Record) error
 WithAttrs(attrs []Attr) Handler
 WithGroup(name string) Handler
}
Salin selepas log masuk
Output:
{"time":"2023-01-23T11:02:27.1606485+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦"}
Salin selepas log masuk

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

日志选项

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

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

type HandlerOptions struct {
 AddSource bool
 Level Leveler
 ReplaceAttr func(groups []string, a Attr) Attr
}
Salin selepas log masuk

通过 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()))
}
Salin selepas log masuk

输出:

{"time":"2023-01-23T15:36:14.8610328+08:00","level":"INFO","msg":"记录日志-info","公众号":"Golang来啦","time":0}
Salin selepas log masuk

这样的话,我们就可以自定义 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()))
}
Salin selepas log masuk

输出:

{"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}
Salin selepas log masuk

从输出可以看到记录日志的时候显示了来源,同时也记录了 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来啦")
}
Salin selepas log masuk

输出:

{"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来啦"}
Salin selepas log masuk

另外,如果设置里默认的 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")
}
Salin selepas log masuk

输出:

{"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
Salin selepas log masuk

两种记录日志的方式

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

logger.Info("Go is best language!", "公众号", "Golang来啦")
Salin selepas log masuk

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

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

logger.LogAttrs(slog.LevelInfo, "Go is best language!", slog.String("公众号", "Golang来啦"))
Salin selepas log masuk

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

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

String
Int64
Int
Uint64
Float64
Bool
Time
Duration
Salin selepas log masuk

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

logger.LogAttrs(slog.LevelInfo, "Go is best language!", slog.String("公众号", "Golang来啦"), slog.Int("age", 18))
Salin selepas log masuk

输出:

{"time":"2023-01-23T11:45:11.7921124+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦","age":18}
Salin selepas log masuk

如何绑定一组属性

学到这里我就在想,假如我想在一个 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"),
 )
}
Salin selepas log masuk

输出:

{"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"}
Salin selepas log masuk

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"))

...
Salin selepas log masuk

如果想上面这样,每次都记录一次 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()))
}
Salin selepas log masuk

输出:

{"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}
Salin selepas log masuk

从输出可以看到两条日志都记录了 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"))
}
Salin selepas log masuk

执行程序并访问地址: 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}
Salin selepas log masuk

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

slog: Bagaimanakah perkembangan pakej log berstruktur rasmi Go berjalan? Bagaimana cara menggunakannya?

我很好奇作者为什么把这两个方法移除掉,后面翻到 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
}
Salin selepas log masuk

输出:

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

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

func (h *LogrusHandler) Handle(rec slog.Record) error {}
Salin selepas log masuk

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

总结

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

Atas ialah kandungan terperinci slog: Bagaimanakah perkembangan pakej log berstruktur rasmi Go berjalan? Bagaimana cara menggunakannya?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
go
sumber:Golang菜鸟
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan