go-logr/logr

GitHub: go-logr/logr

logr 是一个 Go 语言的极简结构化日志接口库,通过将日志 API 与具体实现解耦,让库和应用开发者能灵活替换日志后端。

Stars: 1398 | Forks: 87

# 一个极简的 Go logging API [![Go Reference](https://pkg.go.dev/badge/github.com/go-logr/logr.svg)](https://pkg.go.dev/github.com/go-logr/logr) [![Go Report Card](https://goreportcard.com/badge/github.com/go-logr/logr)](https://goreportcard.com/report/github.com/go-logr/logr) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/go-logr/logr/badge)](https://securityscorecards.dev/viewer/?platform=github.com&org=go-logr&repo=logr) logr 提供了(另一种)关于 Go 程序和库如何在不与特定 logging 实现 耦合的情况下进行日志记录的观点。这不是一个 logging 的实现—— 它是一个 API。实际上,它是具有两类不同用户的两种 API。 `Logger` 类型专为应用程序和库作者设计。它提供了一个 相对较小的 API,可在你需要输出日志的任何地方 使用。它将实际写入日志(写入文件、stdout 或其他任何位置)的 行为延迟到了 `LogSink` interface。 `LogSink` interface 专为 logging 库实现者设计。它是一个 纯粹的 interface,可由 logging 框架实现以提供 实际的 logging 功能。 这种解耦允许应用程序和库开发者基于 `logr.Logger`(其具有极低的依赖扇出)编写代码,而 logging 的实现则由“栈上游”(例如在 `main()` 内部或附近)管理。 应用程序开发者随后可以根据需要替换实现。 许多人主张库不应该进行日志记录,因此这样的 努力是毫无意义的。欢迎这些人去说服那些 *确实*在写日志的数以万计的库的作者,告诉他们 他们都错了。与此同时,logr 采取了更为实用的方法。 ## 典型用法 在应用程序生命周期的早期,它需要在某处决定 实际上想要使用哪种 logging 库(实现)。类似于: ``` func main() { // ... other setup code ... // Create the "root" logger. We have chosen the "logimpl" implementation, // which takes some initial parameters and returns a logr.Logger. logger := logimpl.New(param1, param2) // ... other setup code ... ``` 大多数应用程序会调用其他库,创建结构来控制流程, 等等。`logr.Logger` 对象可以传递给这些其他库,存储 在 struct 中,甚至在需要时用作包级全局变量。例如: ``` app := createTheAppObject(logger) app.Run() ``` 除了这种早期设置之外,其他包不需要知道关于 实现的选择。它们使用接收到的 `logr.Logger` 来写入日志: ``` type appObject struct { // ... other fields ... logger logr.Logger // ... other fields ... } func (app *appObject) Run() { app.logger.Info("starting up", "timestamp", time.Now()) // ... app code ... ``` ## 背景 如果 Go 标准库定义了用于 logging 的 interface,这个项目 可能就不需要了。唉,现实并非如此。 当 Go 开发者开始使用 [slog](https://github.com/golang/go/issues/56345) 开发这样的 interface 时,他们采纳了 logr 的一些设计,但也舍弃了部分功能并更改了其他部分: | 功能 | logr | slog | |---------|------|------| | 高级 API | `Logger`(按值传递) | `Logger`(按[指针](https://github.com/golang/go/issues/59126)传递) | | 底层 API | `LogSink` | `Handler` | | 栈展开 | 由 `LogSink` 完成 | 由 `Logger` 完成 | | 跳过辅助函数 | `WithCallDepth`, `WithCallStackHelper` | [Logger 不支持](https://github.com/golang/go/issues/59145) | | 按需生成用于日志记录的值 | `Marshaler` | `LogValuer` | | 日志级别 | >= 0,数值越大意味着“越不重要” | 正数和负数,0 表示“info”,数值越大意味着“越重要” | | 错误日志条目 | 始终记录,没有详细级别 | 带有 >= `LevelError` 级别的普通日志条目 | | 通过 context 传递 logger | `NewContext`, `FromContext` | 无 API | | 为 logger 添加名称 | `WithName` | 无 API | | 在调用链中修改日志条目的详细程度 | `V` | 无 API | | 键值对分组 | 不支持 | `WithGroup`, `GroupValue` | | 传递 context 以提取附加值 | 无 API | 诸如 `InfoCtx` 的 API 变体 | 高级 slog API 明确被设计为能够分层构建在共享 `slog.Handler` 之上的众多不同 API 之一。logr 就是这样一个 替代 API,通过一些转换函数提供[互操作性](#slog-interoperability)。 ### 灵感 在你考虑这个包之前,请阅读[举世无双的 Dave Cheney 写的这篇博客文章][warning-makes-no-sense]。我们非常赞赏他 所说的话,并且这与我们自己的经验在很大程度上是一致的。 ### 与 Dave 的理念的差异 主要区别在于: 1. Dave 基本上提议废除 logging API 的概念,转而支持 `fmt.Printf()`。我们不同意这一点,特别是当你考虑到输出 位置、时间戳、文件和行装饰以及结构化 logging 时。这个 包将 logging API 限制为仅 2 种类型的日志:info 和 error。 Info 日志是你想要告诉用户的非错误信息。Error 日志则是错误。如果你的代码从下级 函数调用接收到一个 `error`,并且正在记录该 `error` *且不返回它*,请使用 error 日志。 2. Info 日志上的详细级别。这为开发者提供了一个机会,可以 为 info 日志指示任意级别的重要性,而无需分配具有 语义含义的名称,例如“warning”、“trace”和“debug”。表面上这 可能感觉非常相似,但主要区别在于缺乏语义。 因为详细程度是一个数值,所以可以安全地假设,在更高 详细程度下运行的应用程序会生成更多(且更不重要)的日志。 ## 实现(非详尽列表) 以下 logging 库提供了相应的实现: - **一个函数**(可桥接到非结构化库):[funcr](https://github.com/go-logr/logr/tree/master/funcr) - **一个 testing.T**(用于 Go 测试,带有类似 JSON 的输出):[testr](https://github.com/go-logr/logr/tree/master/testr) - **github.com/google/glog**:[glogr](https://github.com/go-logr/glogr) - **k8s.io/klog**(用于 Kubernetes):[klogr](https://git.k8s.io/klog/klogr) - **一个 testing.T**(带有类似 klog 的文本输出):[ktesting](https://git.k8s.io/klog/ktesting) - **go.uber.org/zap**:[zapr](https://github.com/go-logr/zapr) - **log**(Go 标准库 logger):[stdr](https://github.com/go-logr/stdr) - **github.com/sirupsen/logrus**:[logrusr](https://github.com/bombsimon/logrusr) - **github.com/wojas/genericr**:[genericr](https://github.com/wojas/genericr)(方便实现你自己的后端) - **logfmt**(Heroku 风格的[日志记录](https://www.brandur.org/logfmt)):[logfmtr](https://github.com/iand/logfmtr) - **github.com/rs/zerolog**:[zerologr](https://github.com/go-logr/zerologr) - **github.com/go-kit/log**:[gokitlogr](https://github.com/tonglil/gokitlogr)(自 v0.12.0 起也兼容 github.com/go-kit/kit/log) - **bytes.Buffer**(写入缓冲区):[bufrlogr](https://github.com/tonglil/buflogr)(用于确保值被记录,例如在测试期间) ## slog 互操作性 互操作性是双向的:在 `slog.Handler` 中使用 `logr.Logger` API,以及在 `logr.LogSink` 中使用 `slog.Logger` API。`FromSlogHandler` 和 `ToSlogHandler` 可在 `logr.Logger` 和 `slog.Handler` 之间进行转换。 像往常一样,`slog.New` 可用于将这样的 `slog.Handler` 包装在高级 slog API 中。 ### 使用 `logr.LogSink` 作为 slog 的后端 如果两者都受支持,日志调用可以从高级 API 直接传递到后端,而无需转换参数。`FromSlogHandler` 和 `ToSlogHandler` 可以来回转换而不增加额外的包装器,但有一个例外:当使用 `Logger.V` 为 `slog.Handler` 调整详细程度时,`ToSlogHandler` 必须使用一个包装器来为未来的日志调用调整详细程度。 不支持 slog 有几个缺点: - 如果 handler 是通过 `slog.Logger` 调用的,记录源代码位置可以正常工作,但在其他情况下可能会出错。这是因为 `logr.Sink` 会自己进行栈展开,而不是使用高级 API 提供的程序计数器。 - slog 级别 <= 0 可以通过取反级别映射到 logr 级别而不会丢失信息。但所有 > 0 的 slog 级别(例如 `slog.Logger.Warn` 使用的 `slog.LevelWarning`)在调用 sink 之前必须映射到 0,因为 logr 不支持“比 info 更重要”的级别。 - slog 的 group 概念受支持的方式是在键/值对中的每个键前加上 group 名称,并以点分隔。对于像 JSON 这样的结构化输出,最好将键/值对分组在一个对象内。 - 特殊的 slog 值和 interface 无法按预期工作。 - 其开销可能更高。 这些缺点非常严重,以至于混合使用 slog 和 logr 的应用程序应该切换到不同的后端。 ### 使用 `slog.Handler` 作为 logr 的后端 - 所有 logr 详细级别都可以 1:1 地映射到对应的 slog 级别(通过取反)。 - 栈展开由 `SlogSink` 完成,并将生成的程序计数器传递给 `slog.Handler`。 - 通过 `Logger.WithName` 添加的名称将被收集,并记录在一个附加属性中,其键为 `logger`,值为以斜杠分隔的名称。 - `Logger.Error` 被转换为一条以 `slog.LevelError` 为级别的日志记录,并且如果提供了 error,还会记录一个键为 `err` 的附加属性。 主要的缺点是不支持 `logr.Marshaler`。理想情况下,类型应该 同时支持 `logr.Marshaler` 和 `slog.Valuer`。如果与 不支持 slog 的 logr 实现保持兼容性并不重要,那么 `slog.Valuer` 就足够了。 ## 常见问题 ### 概念 #### 为什么使用结构化 logging? - **结构化日志更容易查询**:由于你拥有键值对, 通过筛选特定键的内容,查询结构化日志中的 特定值要容易得多—— 比如在请求日志中搜索错误代码,在 Kubernetes reconciler 中搜索 已协调对象的名称和 namespace 等。 - **结构化 logging 使得拥有交叉引用的日志变得更容易**: 与可搜索性类似,如果你对键保持约定, 收集与特定概念相关的所有日志行就会变得很容易。 - **结构化日志允许更好的过滤维度**:如果你的 日志具有结构,你就可以更精确地控制记录多少 信息——你可能会在特定配置中选择 记录特定的键而不记录其他键,仅记录特定键 匹配特定值的日志行等,而不是仅仅依靠 v 级别和名称 来进行过滤。 - **结构化日志更好地表示结构化数据**:有时, 你想要记录的数据本质上是结构化的(想想元组链 对象)。结构化日志允许你在输出时保留该结构。 #### 为什么使用 V-levels? **V-levels 为运维人员提供了一种简单的方法来控制日志操作的冗长程度**。V-levels 为给定包提供了一种方式,以区分 给定日志消息的相对重要性或冗长程度。这样,如果 特定的 logger 或包记录了太多的消息,包的使用者 只需更改该库的 v-levels 即可。 #### 为什么不使用命名级别,比如 Info/Warning/Error? 阅读 [Dave Cheney 的文章][warning-makes-no-sense]。然后阅读[与 Dave 的理念的差异](#differences-from-daves-ideas)。 #### 为什么也不允许格式化字符串? **格式化字符串抵消了结构化日志的许多好处**: - 如果不求助于模糊搜索、 正则表达式等,它们不容易被搜索。 - 它们不能很好地存储结构化数据,因为内容被扁平化为 一个字符串。 - 它们不可交叉引用。 - 它们不易被压缩,因为消息不是常量。 (除非你将位置参数转换为带有数字 键的键值对,此时你得到的是带有无意义 键的键值对日志。) ### 实践 #### 为什么使用键值对,而不是 map? 键值对*更*容易进行优化,尤其是在内存分配方面。Zap(一个启发了 logr 接口的结构化 logger)有 [性能测量](https://github.com/uber-go/zap#performance) 很好地证明了这一点。 虽然接口最终变得不那么直观,但你获得了 潜在的更好性能,并且避免了用户每次想 记录日志时都输入 `map[string]string{}`。 #### 如果我的 V-levels 在不同的库之间有所不同怎么办? 那没关系。在每个 logger 的基础上控制你的 V-levels,并使用 `WithName` 方法将不同的 logger 传递给不同的库。 通常,你应该注意确保在给定的 logger 内具有相对 一致的 V-levels,然而,因为这会让你更容易决定 请求何种冗长程度的日志。 #### 但我真的很想使用格式化字符串! 这实际上不是一个问题。假设你的问题是“我该如何 将使用格式化字符串进行日志记录的心智模型转换为使用 常量消息进行日志记录”: 1. 弄清楚错误实际上是什么,就像你以 TL;DR 风格写的那样, 并将其用作消息。 2. 对于每一个你要编写格式说明符的地方,查看它前面的单词, 并将其添加为键值对。 例如,考虑以下示例(取自 Kubernetes 代码库): - `klog.V(4).Infof("Client is returning errors: code %v, error %v", responseCode, err)` 变为 `logger.Error(err, "client returned an error", "code", responseCode)` - `klog.V(4).Infof("Got a Retry-After %ds response for attempt %d to %v", seconds, retries, url)` 变为 `logger.V(4).Info("got a retry-after response when requesting url", "attempt", retries, "after seconds", seconds, "url", url)` 如果你*真的*必须使用格式化字符串,请在键的值中使用它,并 自己调用 `fmt.Sprintf`。例如:`log.Printf("unable to reflect over type %T")` 变为 `logger.Info("unable to reflect over type", "type", fmt.Sprintf("%T"))`。不过总的来说,需要 这样做的情况应该很少见。 #### 我该如何选择我的 V-levels? 这基本上是唯一的硬性约束:增加 V-levels 以表示 更冗长或更偏向调试的日志。 除此之外,你可以从 `0` 开始,表示“你总是想看到这个”, `1` 表示“你可能*有可能*想关闭的常见日志记录”,而 `10` 表示“我想对你的日志收集栈进行性能测试”。 然后根据需要逐渐选择介于两者之间的级别,从 10 开始向下(用于调试和 trace 风格的日志),从 1 开始向上(用于更冗长的 info 类日志)。作为参考,slog 预定义了 -4 用于调试日志 (对应于 logr 中的 4),这与 [Kubernetes 推荐](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#what-method-to-use)的内容相匹配。 #### 我该如何选择我的键? 键相当灵活,几乎可以保存任何字符串 值。为了与实现获得最佳兼容性,并与其他项目中 现有的代码保持一致,你应该考虑一些约定。 - 使你的键具备人类可读性。 - 常量键通常是个好主意。 - 在你的代码库中保持一致。 - 键应该自然匹配消息字符串的各个部分。 - 对简单的键使用小写,对 更复杂的键使用 [lowerCamelCase](https://en.wiktionary.org/wiki/lowerCamelCase)。Kubernetes 就是 [采用了该约定]的项目之一(https://github.com/kubernetes/community/blob/HEAD/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments)。 虽然键名基本上是不受限制的(空格也是可接受的), 但坚持使用可打印的 ASCII 字符通常是个好主意,或者至少 匹配你日志行的一般字符集。 #### 为什么键应该是常量值? 结构化日志记录的重点是让随后的日志处理变得更简单。你的键实际上就是每条日志消息的 schema。如果你在同一个日志行的不同实例中使用不同的 键,就会让你的结构化日志变得 非常难以使用。`Sprintf()` 是用于值的,而不是用于键的! #### 为什么这不是一个纯粹的 interface? Logger 类型是作为 struct 实现的,以便允许 Go 编译器优化诸如未被触发的 高 V 级别的 `Info` 日志之类的内容。并非所有这些 实现都已完成,但这种结构被建议作为 一种确保它们*可以*被实现的方法。所有真正的 工作都在 `LogSink` interface 背后。
标签:EVTX分析, 子域名突变, 日志审计