Go1.21标准库 slog,日志新选择!

详解 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 提供了两种格式化输出,都需要显式创建。

  1. key=value 形式
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这些预定义公共变量。

  1. json 对象形式
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}

自定义 Handler

通过上面的两种格式化输入,我们知道了 slog 是通过 NewXXXHandler 来实现不同格式的输出的。除了以上两种 Handler,slog 也可以自定义 Handler 来实现个性化的输出格式。 实际上就是实现 slog.Handler 接口,log/slog的作者 Jonathan Amsterdam 提供了一篇slog 自定义 handler 指南供大家参考。

设置默认 Logger

我们每次都要用log0log1来调用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: 订单服务"

日志级别和 Source

上文说过,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":"订单服务"}

公共属性

如果你的每条日志都需要输出nameservice,那就有必要将它们变为公共属性字段,这样做可以缓存格式化结果,不用每次输出一条日志都格式化一次。从而避免重复格式化带来的性能损耗。使用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 的性能更高。

  1. 使用 Logger.With 避免重复格式化公共属性字段,让程序缓存格式化结果,不用每次输出日志都格式化一次。
  2. 将昂贵的计算推迟到日志输出时再进行,例如传递指针而不是格式化后的字符串。这可以避免在禁用的日志行上进行不必要的工作。
  3. 对于昂贵的值,可以实现 LogValuer 接口,这样在输出时可以进行 lazy 加载计算。

输出到文件

你肯定发现了,上面所有的 Handler 都是把日志输出到os.Stdout,这是因为在 k8s 广泛使用的背景下,是不需要将日志输出到文件的,在 Pod 里都是输出到os.Stdoutos.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 等,如果有需要,你可以自行探索了。

  • 原创
  • 学分: 10
  • 分类: Go
  • 标签: slog  go 
点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
认知那些事
认知那些事
0x2b62...95a0
人立于天地之间,必然有我们的出路。