go-logr/logr
GitHub: go-logr/logr
logr 是一个 Go 语言的极简结构化日志接口库,通过将日志 API 与具体实现解耦,让库和应用开发者能灵活替换日志后端。
Stars: 1398 | Forks: 87
# 一个极简的 Go logging API
[](https://pkg.go.dev/github.com/go-logr/logr)
[](https://goreportcard.com/report/github.com/go-logr/logr)
[](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分析, 子域名突变, 日志审计