使用slog进行结构化日志记录

Go 1.21.0 中,Go 标准库新增结构化日志记录包 log/slog,结构化日志采用键值对的形式,因此可以被快速、可靠地解析、过滤、搜索和分析。对于服务器而言,日志记录是开发人员观察系统详细行为的重要方式,通常是调试的首选位置。因此,日志记录往往是容易占用大量存储空间的,快速搜索和筛选日志内容非常重要。

自 Go 的初版发布以来,标准库就一直有一个日志包 log。随着时间的推移,我们逐渐了解到结构化日志对 Go 程序员非常重要。它在我们每年的调查中总是排名靠前,Go 生态系统中的许多包提供了结构化日志功能。其中一些包非常受欢迎:Go 的第一个结构化日志包之一 logrus,已在超过 100,000 个其他包中使用。

由于有许多结构化日志包可供选择,大型程序经常会通过它们的依赖项包含多个包。主程序可能必须配置每个日志包,以使日志输出保持一致:所有日志都以相同的格式输出到相同的位置。将结构化日志引入标准库中,我们可以提供一个共同的框架,所有其他结构化日志包都可以共享。

slog之旅

下面是一个简单示例,展示了如何使用slog进行日志记录:

package main


import "log/slog"


func main() {
    slog.Info("hello, world")
}

输出结果:

2023/08/04 16:09:19 INFO hello, world

Info函数使用默认记录器在Info日志级别上打印一条消息,这个默认记录器实际上是log包中的默认记录器——与您在编写log.Printf时获得的同一个记录器。这解释了为什么输出看起来如此相似:只有"INFO"是新添加的。slog和原始log包可以一起工作,因此很容易上手。

除了Info之外,还有三个其他级别的函数——Debug、Warn和Error,以及一个更通用的Log函数,它采用级别作为参数。在slog中,级别只是整数,因此您不仅限于这四个命名级别。例如,Info的级别是零,Warn的级别是4,因此如果您的日志系统在这些级别之间有一个级别,您可以使用2。

与 log 包不同,我们可以通过在消息后面添加键值对来轻松地向输出中添加键值对:

slog.Info("hello, world", "user", os.Getenv("USER"))

现在输出看起来像这样:

2023/08/04 16:27:19 INFO hello, world user=jba

正如我们提到的那样,slog 的顶层函数使用默认记录器。我们可以显式地获取此记录器,并调用其方法。

logger := slog.Default()
logger.Info("hello, world", "user", os.Getenv("USER"))

每个顶级函数对应于 slog.Logger 上的一个方法。输出与之前一样。

最初,slog 的输出经过默认的 log.Logger,产生我们上面看到的输出。我们可以通过更改记录器使用的处理程序来更改输出。slog 提供了两个内置处理程序。TextHandler 以 key=value 的形式发出所有日志信息。该程序使用 TextHandler 创建一个新的记录器,并对 Info 方法进行相同调用。

logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("hello, world", "user", os.Getenv("USER"))

现在输出看起来像这样:

time=2023-08-04T16:56:03.786-04:00 level=INFO msg="hello, world" user=jba

所有内容都已转换为键值对,字符串需要在必要时用引号引起来以保留其结构。

若要进行 JSON 输出,请安装内置的 JSONHandler。

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("hello, world", "user", os.Getenv("USER"))

现在我们的输出是一系列的 JSON 对象,每次日志调用生成一个对象。

{"time":"2023-08-04T16:58:02.939245411-04:00","level":"INFO","msg":"hello, world","user":"jba"}

您不仅限于使用内置的处理程序。任何人都可以通过实现 slog.Handler 接口来编写处理程序。处理程序可以生成特定格式的输出,或者可以包装另一个处理程序以增加功能。slog 文档中的一个示例展示了如何编写一个包装处理程序,用于更改日志消息的最低级别显示。

目前我们一直使用的交替键值语法对于频繁执行的日志语句可能更加高效,但使用 Attr 类型并调用 LogAttrs 方法可能更节省内存分配。这些方法可以一起使用来尽量减少内存的分配。有一些函数可用于将字符串、数字和其他常见类型构建为 Attr。通过调用 LogAttrs 方法,可以以更快的速度产生与上述相同的输出结果。

slog.LogAttrs(context.Background(), slog.LevelInfo, "hello, world",
    slog.String("user", os.Getenv("USER")))

slog 还有很多其他功能:

  • 正如对 LogAttrs 的调用所示,你可以将一个 context.Context 传递给一些日志函数,这样处理程序就可以提取上下文信息,例如跟踪ID(即使取消该上下文也不会阻止写入日志条目)。

  • 你可以调用 Logger.With 来给日志记录器添加属性,这些属性将出现在其所有输出中,有效地将多个日志语句的共同部分提取出来。这不仅方便,而且可以提高性能,下面会讨论这一点。

  • 属性可以组合成组。这样可以为日志输出添加更多结构,有助于消除原本相同的键之间的歧义。

  • 你可以通过使用 LogValue 方法来控制日志中数值的显示方式。这可以用于将结构体的字段作为一组进行记录,或者对敏感数据进行隐藏处理,等等。

性能

我们希望 slog 的性能很高。为了获得大规模的性能提升,我们设计了 Handler 接口,以提供优化的机会。Enabled 方法在每个日志事件的开始处被调用,给处理程序提供了快速丢弃不需要的日志事件的机会。WithAttrs 和 WithGroup 方法允许处理程序在每次日志调用时只格式化一次由 Logger.With 添加的属性。当大型属性(如 http.Request)被添加到 Logger 中并在多个日志调用中使用时,这种预格式化能够显著提高性能。

为了了解现有开源项目中日志记录的典型模式,我们进行了性能优化工作。我们发现超过95%的日志调用传递了五个或更少的属性。我们还对属性的类型进行了分类,发现少数几种常见类型占了大多数。然后,我们编写了能够捕捉常见情况的基准测试,并将其用作了解时间消耗的指南。最大的性能提升来自于对内存分配的仔细关注。

设计过程

slog 包是自 2012 年 Go 1 发布以来对标准库的最大补充之一。我们希望花费足够的时间来设计它,同时我们知道社区的反馈意见至关重要。

到 2022 年 4 月,我们已经收集到了足够的数据,以向 Go 社区展示结构化日志记录的重要性。Go 团队决定探索将其添加到标准库中。

我们开始研究现有的结构化日志记录包的设计方式。我们还利用存储在 Go 模块代理上的大量开源 Go 代码来了解这些包的实际使用情况。我们的第一个设计就是基于这项研究以及 Go 的简约精神。我们希望提供一个页面简洁易懂、性能良好的 API。

我们从未打算取代现有的第三方日志记录包。它们在本职工作上表现良好,而替换现有的工作正常的代码很少是开发人员有效利用时间的好方法。我们将 API 分成了前端 Logger 和后端接口 Handler。这样,现有的日志记录包可以与共同的后端进行通信,从而使用这些包的其他包可以互操作而无需重写。可以为许多常见的日志记录包编写或正在进行 Handlers,包括 Zap、logr 和 hclog。

我们与 Go 团队和其他有丰富日志记录经验的开发人员共享了我们的初步设计。我们根据他们的反馈做出了修改,并在 2022 年 8 月感觉到我们已经拥有了可行的设计。2022 年 8 月 29 日,我们公开发布了实验性实现,并开始了在 GitHub 上的讨论,以听取社区的声音。回应热烈且大多数积极。由于其他结构化日志记录包的设计师和用户的深思熟虑的评论,我们进行了一些更改并添加了一些功能,例如分组和 LogValuer 接口。我们两次更改了日志级别到整数的映射。

经过两个月的时间和约300条评论,我们觉得我们已经准备好提出一个真正的建议和相关的设计文档了。这个建议引起了800多条评论,并对API和实现进行了许多改进。这里有两个API变更的例子,都涉及到context.Context:

1.最初的API支持将日志记录器添加到上下文中。许多人认为这是一种方便的方式,可以轻松地将日志记录器传递到不关心它的代码层级中。但其他人认为这会引入隐含的依赖性,使代码更难理解。最终,我们移除了这个功能,因为它引起了太多争议。

2.我们还在相关问题上努力思考传递上下文给日志记录方法的方式,尝试了许多设计。最初,我们抵制了将上下文作为第一个参数传递的标准模式,因为我们不希望每个日志记录调用都需要一个上下文,但最终我们创建了两组日志记录方法,一组带有上下文,一组不带上下文。

另一个我们没有进行的更改涉及到用交替键值语法表达属性:

slog.Info("message", "k1", v1, "k2", v2)

许多人坚决认为这是个坏主意。他们觉得这样的语法难以阅读,并且容易由于遗漏键或值而出错。他们更喜欢使用显式属性来表达结构:

slog.Info("message", slog.Int("k1", v1), slog.String("k2", v2))

但我们认为轻量级的语法对于使Go易于使用和有趣尤为重要,尤其是对于新的Go程序员。我们也知道几个Go日志包,比如logr、go-kit/log和zap(使用SugaredLogger)成功地使用了交替的键值。我们添加了一个vet检查来捕捉常见错误,但并没有改变设计。

在2023年3月15日,该提案被接受,但仍有一些小的未解决问题。接下来的几周里,提出和解决了十个额外的改动。到7月初,log/slog包的实现已经完成,包括用于验证处理程序的testing/slogtest包,以及用于正确使用交替键值的vet检查。

在2023年8月8日,Go 1.21发布,同时也发布了slog。我们希望您发现它有用,并且使用起来像构建它一样有趣。

想要了解Go更多内容,欢迎扫描下方👇关注公众号,回复关键词 [实战群]  ,就有机会进群和我们进行交流


e9b272a141aa363f9258f40097349f09.png

分享、在看与点赞Go 35b47a24ae061dad3b3905b2e309dd4a.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值