{"content":{"title":"Go1.21标准库 slog，日志新选择！","body":"Go1.21 已经发布半年多了，今天终于有时间来研究下期待很久的 slog 包，这是在 Go1.21 版本新加入的标准库，有了slog，以后再也不用纠结用哪个日志包了，如果是新项目，没有历史包袱，直接采用官方日志包 slog 即可！\r\n\r\n接下来我们开始探索 slog 的用法。\r\n\r\n### 快速入门\r\n\r\n先定义两个字段，用于打印日志。\r\n\r\n```go\r\nconst name = \"认知那些事\"\r\nconst service = \"订单服务\"\r\n```\r\n\r\n如果你只想用 slog 打印一些简单日志，调试程序，下面是一个例子\r\n\r\n```go\r\nimport (\r\n    \"log/slog\"\r\n)\r\n\r\nfunc main() {\r\n    slog.Debug(\"Hello\", \"name\", name, \"service\", service)\r\n    slog.Info(\"Hello\", \"name\", name, \"service\", service)\r\n    slog.Warn(\"Hello\", \"name\", name, \"service\", service)\r\n    slog.Error(\"Hello\", \"name\", name, \"service\", service)\r\n}\r\n```\r\n\r\n如你所看到的，slog 提供了 4 个级别的日志，参数传递方式为`(msg, key1, value1, key2, value2...)`, 运行上面的代码，输出如下：\r\n\r\n```\r\n2024/01/19 12:21:24 INFO Hello name=认知那些事 service=订单服务\r\n2024/01/19 12:21:24 WARN Hello name=认知那些事 service=订单服务\r\n2024/01/19 12:21:24 ERROR Hello name=认知那些事 service=订单服务\r\n```\r\n\r\n你可能会奇怪，为啥没有 DEBUG 级别的日志呢？是的，slog 默认只输出 INFO 级别以上日志，需要手动设置日志级别才能输出 DEBUG 日志，后面会讲解。\r\n\r\n### 格式化输出\r\n\r\nslog 默认的日志输出只适合用在控制台打印，用于开发调试。在生产环境，我们一般都需要监控日志、分析日志，因此需要格式化输出，slog 提供了两种格式化输出，都需要显式创建。\r\n\r\n1. key=value 形式\r\n\r\n```go\r\nh0 := slog.NewTextHandler(os.Stdout, nil)\r\nlog0 := slog.New(h0)\r\nlog0.Info(\"Hello\", \"name\", name, \"service\", service)\r\nlog0.Error(\"访问网络失败\", \"err\", net.ErrClosed, \"status\", 500)\r\n```\r\n\r\n运行代码，输出如下：\r\n\r\n```\r\ntime=2024-01-19T12:21:24.609+08:00 level=INFO msg=Hello name=认知那些事 service=订单服务\r\ntime=2024-01-19T12:21:24.609+08:00 level=ERROR msg=访问网络失败 err=\"use of closed network connection\" status=500\r\n```\r\n\r\n可以看到，输出的日志全都变成了 key=value 形式。包括`time`, `level`, `msg`这些预定义公共变量。\r\n\r\n2. json 对象形式\r\n\r\n```go\r\nh1 := slog.NewJSONHandler(os.Stdout, nil)\r\nlog1 := slog.New(h1)\r\nlog1.Info(\"Hello\", \"name\", name, \"service\", service)\r\nlog1.Error(\"访问网络失败\", \"err\", net.ErrClosed, \"status\", 500)\r\n```\r\n\r\n运行代码，输出如下：\r\n\r\n```\r\n{\"time\":\"2024-01-19T12:21:24.610018+08:00\",\"level\":\"INFO\",\"msg\":\"Hello\",\"name\":\"认知那些事\",\"service\":\"订单服务\"}\r\n{\"time\":\"2024-01-19T12:21:24.610027+08:00\",\"level\":\"ERROR\",\"msg\":\"访问网络失败\",\"err\":\"use of closed network connection\",\"status\":500}\r\n```\r\n\r\n### 自定义 Handler\r\n\r\n通过上面的两种格式化输入，我们知道了 slog 是通过 `NewXXXHandler` 来实现不同格式的输出的。除了以上两种 Handler，slog 也可以自定义 Handler 来实现个性化的输出格式。 实际上就是实现 `slog.Handler` 接口，`log/slog`的作者 Jonathan Amsterdam 提供了一篇[slog 自定义 handler 指南](https://github.com/golang/example/blob/master/slog-handler-guide/guide.md)供大家参考。\r\n\r\n### 设置默认 Logger\r\n\r\n我们每次都要用`log0`或`log1`来调用`Info、Error`等来输出日志，不仅麻烦，还要费尽心思命名，你可以设置默认 Logger 来解决这个问题。\r\n\r\n```go\r\nslog.SetDefault(log0) // 将上面的log0设置为默认Logger\r\n```\r\n\r\n将默认 Logger 设置为 log0 后（你也可以设置为 log1，请随意），每次调用`slog.Info`等都是用 log0 的`key=value`形式输出日志，而且有点意外的是，log 包也会受影响，请看下面代码例子：\r\n\r\n```go\r\n// 标准库 slog包\r\nslog.Info(\"默认Logger\", \"name\", name, \"service\", service)\r\n\r\n// 标准库 log包\r\nlog.Printf(\"标准库log包，使用了slog设置的默认Logger，name: %s，service: %s\", name, service)\r\n```\r\n\r\n运行代码，输出如下：\r\n\r\n```\r\ntime=2024-01-19T12:21:24.610+08:00 level=INFO msg=默认Logger name=认知那些事 service=订单服务\r\ntime=2024-01-19T12:21:24.610+08:00 level=INFO msg=\"标准库log包，使用了slog设置的默认Logger，name: 认知那些事，service: 订单服务\"\r\n```\r\n\r\n### 日志级别和 Source\r\n\r\n上文说过，slog 默认输出 INFO 以上级别日志，我们可以自定义日志输出级别。另外，大多数情况下我们还需要知道日志在源文件中的位置，通过把`AddSource`设置为`true`来实现。\r\n\r\n```go\r\nopts := slog.HandlerOptions{\r\n    AddSource: true,\r\n    Level:     slog.LevelDebug,\r\n}\r\n\r\nslog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &opts)))\r\nslog.Debug(\"设置日志级别\", \"name\", name, \"service\", service)\r\n```\r\n\r\n运行代码，即可看到`DEBUG`日志和所在的源文件位置(`source`)都输出了：\r\n\r\n```\r\n{\"time\":\"2024-01-19T12:21:24.610052+08:00\",\"level\":\"DEBUG\",\"source\":{\"function\":\"main.main\",\"file\":\"/Users/xxx/code/demo/slog/main.go\",\"line\":63},\"msg\":\"设置日志级别\",\"name\":\"认知那些事\",\"service\":\"订单服务\"}\r\n```\r\n\r\n### 公共属性\r\n\r\n如果你的每条日志都需要输出`name`，`service`，那就有必要将它们变为公共属性字段，这样做可以缓存格式化结果，不用每次输出一条日志都格式化一次。从而避免重复格式化带来的性能损耗。使用`Logger.With`可以做到这一点。\r\n\r\n```go\r\nlog2 := slog.Default().With(\"name\", name, \"service\", service)\r\nslog.SetDefault(log2)\r\nslog.Info(\"公共属性\", \"订单ID\", 1000)\r\nslog.Error(\"公共属性\", \"订单ID\", 1000)\r\n```\r\n\r\n使用`With方法`设置公共属性后，下面输出日志不需要传入 name 和 service 也会输出，运行代码，我们看看效果。\r\n\r\n```\r\n{...,\"msg\":\"公共属性\",\"name\":\"认知那些事\",\"service\":\"订单服务\",\"订单ID\":1000}\r\n{...,\"msg\":\"公共属性\",\"name\":\"认知那些事\",\"service\":\"订单服务\",\"订单ID\":1000}\r\n```\r\n\r\n### 属性群组\r\n\r\n有时候，我们需要把所有属性都放在一个 Json 对象里，使用`WithGroup`可以做到。\r\n\r\n```go\r\nlog3 := log2.WithGroup(\"group\")\r\nslog.SetDefault(log3)\r\nslog.Info(\"属性群组\", \"订单ID\", 999, \"商品名称\", \"Apple\")\r\n```\r\n\r\n运行代码，输出如下：\r\n\r\n```\r\n{...,\"msg\":\"属性群组\",\"name\":\"认知那些事\",\"service\":\"订单服务\",\"group\":{\"订单ID\":1000,\"商品名称\":\"Apple\"}}\r\n```\r\n\r\n注意：公共属性不会被放在 group 里。\r\n\r\n### 一次性输出\r\n\r\n如果你只想让一些属性输出一次，可以用`LogAttrs`来实现，如下：\r\n\r\n```go\r\nslog.LogAttrs(context.Background(), slog.LevelError, \"一次性输出\", slog.String(\"once1\", \"999\"), slog.Int64(\"once2\", 666))\r\n\r\n// 上面的 once1 和 once2 输出一次后，后续都不会再输出\r\nslog.Error(\"once1和once2输出一次后，不再输出\")\r\n```\r\n\r\n### 动态调整日志级别\r\n\r\n在生产环境我们通常将日志级别设置为 Error，当出现 bug 的时候，没有更详细的日志来定位 bug 问题，因此，我们需要动态调整一段时间的日志级别，slog 也支持这个功能，可以通过`slog.LevelVar`来实现：\r\n\r\n```go\r\nvar dlvl slog.LevelVar\r\ndlvl.Set(slog.LevelError) // 设置日志为Error级别\r\n\r\nopts = slog.HandlerOptions{\r\n    AddSource: false,\r\n    Level:     &dlvl,\r\n}\r\nslog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &opts)))\r\n\r\nslog.Info(\"动态调整前\")\r\nslog.Error(\"动态调整前\")\r\n\r\ndlvl.Set(slog.LevelInfo) // 将日志级别动态调整为Info\r\nslog.Info(\"动态调整后\")\r\nslog.Error(\"动态调整后\")\r\n```\r\n\r\n运行上面代码，输出如下：\r\n\r\n```\r\n{\"time\":\"2024-01-19T12:21:24.610114+08:00\",\"level\":\"ERROR\",\"msg\":\"动态调整前\"}\r\n{\"time\":\"2024-01-19T12:21:24.610116+08:00\",\"level\":\"INFO\",\"msg\":\"动态调整后\"}\r\n{\"time\":\"2024-01-19T12:21:24.610118+08:00\",\"level\":\"ERROR\",\"msg\":\"动态调整后\"}\r\n```\r\n\r\n可以看到，动态调整日志级别之前，INFO 日志不会输出，将日志级别动态调整为 Info 后，即可输出 INFO 以上的日志，当不需要 INFO 日志的时候，还可以把日志调整为一开始的 Error 级别。\r\n\r\n### 性能\r\n\r\n根据官方 benchmark 结果，`log/slog`的性能要高于 Go 社区常用的结构化日志包，比如`zap`等。即便如此，还是有一些技巧，让 slog 的性能更高。\r\n\r\n> 1. 使用 Logger.With 避免重复格式化公共属性字段，让程序缓存格式化结果，不用每次输出日志都格式化一次。\r\n> 2. 将昂贵的计算推迟到日志输出时再进行，例如传递指针而不是格式化后的字符串。这可以避免在禁用的日志行上进行不必要的工作。\r\n> 3. 对于昂贵的值，可以实现 LogValuer 接口，这样在输出时可以进行 lazy 加载计算。\r\n\r\n### 输出到文件\r\n\r\n你肯定发现了，上面所有的 Handler 都是把日志输出到`os.Stdout`，这是因为在 k8s 广泛使用的背景下，是不需要将日志输出到文件的，在 Pod 里都是输出到`os.Stdout`和`os.Stderr`。\r\n\r\n然而，如果你是虚拟机或裸机部署，slog 也可以将日志输出到文件。因为`slog.NewXXXHandler`函数的第一个参数是`io.Writer`，传递`文件描述符`就可以向文件写入日志。比较简单，这里就不贴代码了。\r\n\r\n### 日志管理（轮转、压缩、归档和定期清理）\r\n\r\nslog 和常用的日志包一样，自身都不提供日志管理功能，最常用的办法是和`lumberjack`集成来实现，下面给出例子：\r\n\r\n```go\r\nr := &lumberjack.Logger{\r\n    Filename:   \"./run.log\", //\r\n    MaxSize:    1, // 文件最大大小 1M\r\n    MaxAge:     1, // 最大保留时间 1天\r\n    MaxBackups: 3, // 最大保留文件数 3个\r\n    LocalTime:  true, // 是否用本机时间\r\n    Compress:   false, // 是否压缩归档日志\r\n}\r\n\r\nlog4 := slog.New(slog.NewJSONHandler(r, nil))\r\nslog.SetDefault(log4)\r\n```\r\n\r\n至此，本文要讲的 slog 功能已经讲完了，当然还有其他用法，比如将 slog 日志输出到 kafka 等，如果有需要，你可以自行探索了。"},"author":{"user":"https://learnblockchain.cn/people/2540","address":"0x2b624faC1616D08684Cf1d21793c2f39CC1895a0"},"history":null,"timestamp":1705668154,"version":1}