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