Go に詳しい学生は、Go 言語の標準ライブラリのログには、ログ分類がない、構造がない (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来啦
出力は、標準ライブラリ ログの出力と似ています。 slog ライブラリの非常に重要な構造は Logger です。Logger を通じて、Info()、Debug() などのロギング関数を呼び出すことができます。このためのロガーは作成されていないため、デフォルトのものを使用します。クリックしてソース コードを表示できます。
Handler はインターフェイスとして定義されており、slog をよりスケーラブルにすることができます。slog には、TextHandler と TextHandler という 2 つの組み込みの Handler 実装が用意されています。 JSONHandler: さらに、サードパーティのログ パッケージに基づいてハンドラーの実装を定義することも、独自に定義することもできます (これについては後で説明します)。
type Handler interface { Enabled(Level) bool Handle(r Record) error WithAttrs(attrs []Attr) Handler WithGroup(name string) Handler }
TextHandler は、標準ライブラリのログ パッケージと同様に、ログをテキスト行として出力します。
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」の形式で表示されていることがわかります。
上記の 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 级别的日志。
有一点值得注意的是,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,这种记录方式就简洁多了。
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 提案[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 中国語 Web サイトの他の関連記事を参照してください。