原文在这里

由 Jonathan Amsterdam 发布于 22 August 2023

Go 1.21中的新的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,还有三个其他级别的函数 —— DebugWarnError,以及一个更通用的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方法可能更有效。这些一起工作以最小化内存分配。有一些函数可以从字符串、数字和其他常见类型构建Attrs。这个对LogAttrs的调用产生了与上面相同的输出,但是它更快:

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

slog还有很多内容:

  • 如LogAttrs的调用所示,你可以将context.Context传递给一些日志函数,以便处理器可以提取上下文信息,如跟踪ID。(取消上下文并不会阻止日志条目被写入。)
  • 你可以调用Logger.With来向记录器添加将出现在其所有输出中的属性,有效地提取出几个日志语句的公共部分。这不仅方便,而且可以帮助提高性能,如下面所讨论的。
  • 属性可以组合成组。这可以为你的日志输出添加更多的结构,并可以帮助消除那些否则会相同的键的歧义。
  • 你可以通过为其类型提供LogValue方法来控制值在日志中的显示方式。这可以用来将结构的字段作为一组记录,或者删除敏感数据,等等。

了解slog的所有内容的最好地方是这里

性能

我们希望slog能快。为了获得大规模的性能提升,我们设计了Handler接口以提供优化机会。Enabled方法在每个日志事件的开始时被调用,给处理器一个快速丢弃不需要的日志事件的机会。WithAttrsWithGroup方法让处理器一次格式化由Logger.With添加的属性,而不是在每次日志调用时。当大的属性,如http.Request,被添加到Logger然后在许多日志调用中使用时,这种预格式化可以提供显著的加速。

为了指导我们的性能优化工作,我们研究了现有开源项目中的日志记录的典型模式。我们发现超过95%的日志方法调用传递五个或更少的属性。我们还对属性的类型进行了分类,发现少数几种常见类型占了大多数。然后我们编写了捕获常见情况的基准测试,并用它们作为指南来看时间去了哪里。最大的收益来自对内存分配的细心关注。

设计过程

slog包是自2012年Go 1发布以来对标准库的最大的增加之一。我们希望花时间设计它,我们知道社区反馈是必不可少的。

到2022年4月,我们已经收集了足够的数据来证明结构化日志对Go社区的重要性。Go团队决定探索将其添加到标准库。

我们开始研究现有的结构化日志包是如何设计的。我们还利用存储在Go模块代理上的大量开源Go代码,了解这些包实际上是如何使用的。我们的第一个设计是由这项研究以及Go的简单性精神所启发的。我们希望一个在页面上轻便且易于理解的API,而不牺牲性能。

我们从来没有目标是要替换现有的第三方日志包。它们都很擅长自己的工作,替换现有的工作良好的代码很少是开发人员时间的好用途。我们将API分为一个前端,Logger,它调用一个后端接口,Handler。这样,现有的日志包可以与一个公共的后端进行通信,因此使用它们的包可以在不需要重写的情况下进行互操作。许多常见的日志包,包括Zaplogrhclog,都已经编写或正在进行处理器。

我们在Go团队和其他有广泛日志经验的开发人员中分享了我们的初步设计。我们根据他们的反馈做了修改,到2022年8月,我们觉得我们有了一个可行的设计。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日志包,如logrgo-kit/logzap(用它的SugaredLogger)成功地使用了交替的键和值。我们添加了一个vet检查来捕获常见的错误,但没有改变设计。

2023年3月15日,提案被接受,但还有一些小的未解决的问题。在接下来的几周里,提出并解决了十个额外的变更。到7月初,log/slog包的实现完成了,测试/slogtest包用于验证处理器和vet检查用于正确使用交替的键和值。

8月8日,Go 1.21发布了,slog也随之发布。我们希望你发现它有用,使用起来和构建一样有趣。

感谢所有参与讨论和提案过程的人。你们的贡献极大地改进了slog。

资源

log/slog包的文档解释了如何使用它,并提供了几个例子。

wiki页面有Go社区提供的额外资源,包括各种处理器。

如果你想写一个处理器,请参考处理器编写指南


孟斯特

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意