0%

一文搞懂 Golang 高性能日志库 - Zerolog

前言

Zerolog 是一个高性能、零内存分配的 Go 日志库。它为不需要垃圾回收的延迟敏感型应用程序提供结构化日志记录功能。您可以以完全零分配的方式使用,这样在初始化记录器对象后,堆上不会再分配其他对象,从而防止触发垃圾回收。

本教程将解释如何在 Go 应用程序中安装、设置和使用 Zerolog 记录器。我们将首先介绍它的 API 和它提供的所有选项,并展示如何以各种方式自定义它们。最后,我们将描述如何在典型的 Web 应用程序中使用它,以便您可以很好地了解如何在您的项目中采用它。

开始使用 Zerolog

我们可以通过下面的命令安装 Zerolog 到我们的 Go 项目中:

1
go get -u github.com/rs/zerolog/log

该库提供了一个预配置且全局可用的 logger,您可以通过 zerolog/log 软件包在任何文件中导入和使用该 logger

1
2
3
4
5
6
7
8
9
package main

import (
"github.com/rs/zerolog/log"
)

func main() {
log.Info().Msg("Hello from Zerolog global logger")
}

上述程序将 JSON 格式的日志条目输出到控制台:

1
{"level":"info","time":"2023-08-18T13:45:51+01:00","message":"Hello from global logger"}

默认情况下,全局 logger 打印为标准错误,并且它还配置为在级别 TRACE 上记录,尽管您可以通过调用主 zerolog 包提供的 SetGlobalLevel() 函数来设置不同的最小级别:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func main() {
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
log.Info().Msg("Info message")
log.Error().Msg("Error message")
}
1
{"level":"error","time":"2023-08-18T13:51:44+01:00","message":"Error message"}

除了 JSON 日志记录外,您还可以配置 Zerolog 以输出以 CBOR 格式编码的二进制日志。您可以在编译应用程序时使用 binary_log build 标记来启用它:

1
go build -tags binary_log .

执行生成的二进制文件时,您将看到如下所示的输出:

1
�eleveleerrordtime��A�lׂ�p�gmessagemError message�% 

注意:不是我的文章乱码,而是执行生成的二进制文件输出的就是这样的乱码。这是因为它是二进制的格式的,不是 ASCII 格式的。

您可以使用任何 CBOR 解码器(例如 csd)将此二进制日志条目解码为 JSON:

1
./main 2> >(csd)

输出:

1
{"level":"error","time":"2023-08-31T11:57:18.764748096-07:00","message":"Error message"}

我们的系统可能还没有安装 csd,我们可以通过 go install github.com/toravir/csd@0.1.0 命令来进行安装。

如果您的日志条目很大,并且您希望压缩它们以进行长期存储,您可能会发现以 CBOR 格式输出日志很有用,并且在需要时可以毫不费力地转换回 JSON。

探索 Zerolog API

Zerolog 提供了一个简单易懂的结构化日志记录 API。其 Logger 类型表示写入某个 io.Writer 接口的任何活动记录器,对于全局记录器 (zerolog/log),该接口为 os.Stderr 。您可以使用以下 zerolog.New() 方法创建新的自定义 Logger

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"os"

"github.com/rs/zerolog"
)

func main() {
logger := zerolog.New(os.Stdout)
logger.Trace().Msg("Trace message")
}

输出:

1
{"level":"trace","message":"Trace message"}

日志级别

每个记录器都提供了一个对应于 Zerolog 提供的七个日志级别的方法,下面列出了这些级别及其整数优先级值:

  • TRACE(-1):用于跟踪代码执行路径
  • DEBUG(0): 对故障排除有用的信息
  • INFO(1): 描述应用程序正常运行的信息
  • WARNING(2): 对于需要的记录事件,以后可能需要检查
  • ERROR(3): 特定操作的错误信息
  • FATAL(4): 应用程序无法恢复的严重错误。os.Exit(1) 在记录消息后调用
  • PANIC(5): 与 FATAL 类似,但只是名字改成了 PANIC()

您可以通过 Level() 方法将上述任何级别设置为自定义 Logger 的最低级别。您可以传递上面所示的整数优先级数,也可以使用 zerolog 包上提供的级别常量(TraceLevelDebugLevel,等):

1
logger := zerolog.New(os.Stdout).Level(zerolog.InfoLevel)

上述代码片段仅输出比 INFO 更高级别的日志。您还可以通过环境变量设置最小级别(在本教程后面部分演示)。

将上下文数据添加到日志中

当你调用与所选日志级别相对应的方法(Info()Debug()等)时,将返回一个 zerolog.Event 类型,该类型代表一个日志事件。这个 Event 类型提供了几个方法,允许你向其上下文添加属性(以键值对的形式),以便日志条目包含足够的上下文数据,帮助你理解事件。例如,当记录在服务器上创建资源时,你可以在日志中包含用户 ID 或客户端信息(如 IP 地址),这样以后通过这些属性轻松过滤日志。

Event 类型上的大多数方法都返回一个指向 Event 的指针,因此你可以在日志点上根据需要链式调用它们。一旦向 Event 添加了所有必要的上下文,你必须调用其中一个 Msg()Msgf()MsgFunc()Send() 方法来完成 Event 的记录。通常,Msg() 方法将用于通过向日志条目添加消息字段来关闭 Event

1
2
3
4
5
logger.Info().
Str("name", "john").
Int("age", 22).
Bool("registered", true).
Msg("new signup!")

输出:

1
{"level":"info","name":"john","age":22,"registered":true,"message":"new signup!"}

如果要省略该 message 字段,可以调用 Msg() 或改用 Send() 来代替(不传参数):

1
2
3
4
5
logger.Info().
Str("name", "john").
Int("age", 22).
Bool("registered", true).
Send()

输出:

1
{"level":"info","name":"john","age":22,"registered":true}

请注意,必须始终在 zerolog.Event 上最后调用一个事件关闭方法,以便记录相应的条目。如果未使用这些方法,则不会记录日志条目。

1
2
3
4
logger.Info().
Str("name", "john").
Int("age", 22).
Bool("registered", true)

这不会有任何日志记录。

此外,除了要记得调用事件关闭方法外,也不要在一个 event 对象上调用多次事件关闭方法。

向 logger 添加全局上下文

在上一节中,您了解了如何使用 zerolog.Event 类型上的方法将相关上下文添加到日志条目。本节将更进一步,向您展示如何将上下文数据添加到 Logger 其本身,以确保此类数据包含在记录器生成的所有后续记录中。

有两种主要方法可以向 Zerolog Logger 添加上下文。第一种涉及使用返回 zerolog With() 的方法。上下文实例,允许您通过与 zerolog.Event 类型类似的字段方法向键值对中的记录器添加其他属性。然后,在将必要的数据添加到上下文后,必须调用该 Logger() 方法以返回具有更新上下文的新 Logger 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"os"

"github.com/rs/zerolog"
)

func main() {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()

logger.Info().Msg("info message")
logger.Debug().Str("username", "joshua").Send()
}

输出日志:

1
2
{"level":"info","time":"2023-08-31T21:00:29+01:00","message":"info message"}
{"level":"debug","username":"joshua","time":"2023-08-31T21:00:29+01:00"}

上面的代码片段将该 time 字段添加到 生成的所有记录中,这很有意义,因为所有日志记录都应包含时间戳。您还可以将文件和行号添加到所有日志条目中,如下所示:

1
2
3
4
5
6
func main() {
logger := zerolog.New(os.Stdout).With().Timestamp().Caller().Logger()

logger.Info().Msg("info message")
logger.Debug().Str("username", "joshua").Send()
}

输出日志:

1
2
{"level":"info","time":"2023-08-31T21:20:04+01:00","caller":"/home/user/dev/main.go:12","message":"info message"}
{"level":"debug","username":"joshua","time":"2023-08-31T21:20:04+01:00","caller":"/home/user/dev/main.go:13"}

由于该方法返回一个全新的 Logger,您可以使用该 Logger() With() 方法实现子记录器,这些子记录器使用相关元数据注释特定范围内的所有日志 Logger,以将它们与其他记录区分开来。下面是一个人为的例子,演示了这是如何工作的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"os"

"github.com/rs/zerolog"
)

var logger = zerolog.New(os.Stdout).With().Timestamp().Logger()

func main() {
mainLogger := logger.With().Str("service", "main").Logger()
mainLogger.Info().Msg("main logger message")

auth()
admin()
}

func auth() {
authLogger := logger.With().Str("service", "auth").Logger()
authLogger.Info().Msg("auth logger message")
}

func admin() {
adminLogger := logger.With().Str("service", "admin").Logger()
adminLogger.Info().Msg("admin logger message")
}

输出:

1
2
3
{"level":"info","service":"main","time":"2023-08-31T21:13:51+01:00","message":"main logger message"}
{"level":"info","service":"auth","time":"2023-08-31T21:13:51+01:00","message":"auth logger message"}
{"level":"info","service":"admin","time":"2023-08-31T21:13:51+01:00","message":"admin logger message"}

将元数据全局化为 Logger 的第二种方法是使用 UpdateContext() 方法。此方法就地更新 Logger 的内部上下文(无需创建副本),您可以像这样使用它:

1
2
3
4
5
6
7
8
9
func main() {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()

logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("name", "john")
})

logger.Info().Msg("info message")
}

输出日志:

1
{"level":"info","name":"john","time":"2023-09-01T09:25:20+01:00","message":"info message"}

在开发中美化日志

在开发环境中,您可能会发现以更易于阅读的格式从应用程序输出日志条目会很有帮助,这样就可以轻松发现各种事件,而不会被不相关的符号和字段分散注意力。Zerolog 提供了一个 ConsoleWriter 类型,用于解析原始 JSON 条目,并将其以彩色格式输出到控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"os"
"runtime/debug"
"time"

"github.com/rs/zerolog"
)

func main() {
buildInfo, _ := debug.ReadBuildInfo()

logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).
Level(zerolog.TraceLevel).
With().
Timestamp().
Caller().
Int("pid", os.Getpid()).
Str("go_version", buildInfo.GoVersion).
Logger()

logger.Trace().Msg("trace message")
logger.Debug().Msg("debug message")
logger.Info().Msg("info message")
logger.Warn().Msg("warn message")
logger.Error().Msg("error message")
logger.WithLevel(zerolog.FatalLevel).Msg("fatal message")
logger.WithLevel(zerolog.PanicLevel).Msg("panic message")
}

输出:

1
2
3
4
5
6
7
2024-01-26T16:12:09+08:00 TRC main.go:23 > trace message go_version=go1.20.2 pid=41079
2024-01-26T16:12:09+08:00 DBG main.go:24 > debug message go_version=go1.20.2 pid=41079
2024-01-26T16:12:09+08:00 INF main.go:25 > info message go_version=go1.20.2 pid=41079
2024-01-26T16:12:09+08:00 WRN main.go:26 > warn message go_version=go1.20.2 pid=41079
2024-01-26T16:12:09+08:00 ERR main.go:27 > error message go_version=go1.20.2 pid=41079
2024-01-26T16:12:09+08:00 FTL main.go:28 > fatal message go_version=go1.20.2 pid=41079
2024-01-26T16:12:09+08:00 PNC main.go:29 > panic message go_version=go1.20.2 pid=41079

上面这个不够直观,下面这个才是实际效果:

您可以使用 ConsoleWriter 类型上提供的选项来自定义输出的外观和格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: time.RFC3339,
FormatLevel: func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("[%s]", i))
},
FormatMessage: func(i interface{}) string {
return fmt.Sprintf("| %s |", i)
},
FormatCaller: func(i interface{}) string {
return filepath.Base(fmt.Sprintf("%s", i))
},
PartsExclude: []string{
zerolog.TimestampFieldName,
},
}

输出:

1
2
3
4
5
6
7
[TRACE] main.go:41 | trace message | go_version=go1.20.2 pid=41171
[DEBUG] main.go:42 | debug message | go_version=go1.20.2 pid=41171
[INFO] main.go:43 | info message | go_version=go1.20.2 pid=41171
[WARN] main.go:44 | warn message | go_version=go1.20.2 pid=41171
[ERROR] main.go:45 | error message | go_version=go1.20.2 pid=41171
[FATAL] main.go:46 | fatal message | go_version=go1.20.2 pid=41171
[PANIC] main.go:47 | panic message | go_version=go1.20.2 pid=41171

实际效果如下:

请注意,时间戳现在是如何从输出中排除的,以及日志级别、调用方信息和日志消息的格式有何不同。格式化的字段也不再着色,但您可以使用 ptermgookit/color 等库来着色输出。

注意不要在生产环境中使用 , ConsoleWriter 因为它会大大减慢日志记录的速度。它只是为了帮助在开发应用程序时使日志更易于阅读。您可以使用环境变量仅在开发中启用 ConsoleWriter 输出:

1
2
3
4
var output io.Writer = zerolog.ConsoleWriter{...}
if os.Getenv("GO_ENV") != "development" {
output = os.Stderr
}

使用 Zerolog 进行日志采样

采样是一种用于有意删除重复日志条目的技术,以便只保留和处理其中的一部分。当您的高流量应用程序生成大量记录时,这很有帮助,并且存储每一条记录将导致过高的存储和处理成本,这可能是不可取的。采样通过防止每秒记录数百或数千次相同的日志来解决此问题,从而防止资源浪费。

以下是使用 Zerolog 对日志进行采样的最基本方法:

1
2
3
4
5
6
7
8
9
10
11
func main() {
log := zerolog.New(os.Stdout).
With().
Timestamp().
Logger().
Sample(&zerolog.BasicSampler{N: 5})

for i := 1; i <= 10; i++ {
log.Info().Msgf("a message from the gods: %d", i)
}
}

在此示例中,配置为每条日志在五次中仅记录一次。这在 for 循环中进行了演示,其中 INFO 消息通常被记录十次,但由于采样,它只被记录两次:

1
2
{"level":"info","time":"2023-09-02T08:05:48+01:00","message":"a message from the gods: 1"}
{"level":"info","time":"2023-09-02T08:05:48+01:00","message":"a message from the gods: 6"}

Zerolog 提供了其他更复杂的采样器,可能更适合您的应用。例如, BurstSampler 可用于限制在一段时间内记录的日志数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
l := zerolog.New(os.Stdout).
With().
Timestamp().
Logger().
Sample(&zerolog.BurstSampler{
Burst: 3,
Period: 1 * time.Second,
})

for i := 1; i <= 10; i++ {
l.Info().Msgf("a message from the gods: %d", i)
l.Warn().Msgf("warn message: %d", i)
l.Error().Msgf("error message: %d", i)
}
}

在这里, BurstSampler 配置限制每 Logger 秒生成三个以上的日志条目。本来应该在指定范围内记录的所有其他记录都将被丢弃。上面的循环应该记录 30 条消息而不进行采样,但由于上面的 for 配置,它只记录了 3 条:

1
2
3
{"level":"info","time":"2023-09-02T08:20:47+01:00","message":"a message from the gods: 1"}
{"level":"warn","time":"2023-09-02T08:20:47+01:00","message":"warn message: 1"}
{"level":"error","time":"2023-09-02T08:20:47+01:00","message":"error message: 1"}

您只能对特定级别进行抽样,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
burstSampler := &zerolog.BurstSampler{
Burst: 3,
Period: 1 * time.Second,
NextSampler: &zerolog.BasicSampler{N: 5},
}

l := zerolog.New(os.Stdout).
With().
Timestamp().
Logger().
Sample(zerolog.LevelSampler{
WarnSampler: burstSampler,
InfoSampler: burstSampler,
})

在这里,将仅对 INFOWARN 日志进行采样,而其他日志将照常记录,从而产生以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
{"level":"info","time":"2023-09-02T08:26:42+01:00","message":"a message from the gods: 1"}
{"level":"warn","time":"2023-09-02T08:26:42+01:00","message":"warn message: 1"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 1"}
{"level":"info","time":"2023-09-02T08:26:42+01:00","message":"a message from the gods: 2"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 2"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 3"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 4"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 5"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 6"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 7"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 8"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 9"}
{"level":"error","time":"2023-09-02T08:26:42+01:00","message":"error message: 10"}

如您所见, INFO 该消息被记录两次, WARN 记录一次(总共 3 个日志),而所有 10 个 ERROR 日志都被记录下来,因为它没有被采样。如果希望对每个级别进行不同的采样,则必须为每个级别创建不同的采样策略,例如 infoSampler warnSampler 等 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func main() {
infoSampler := &zerolog.BurstSampler{
Burst: 3,
Period: 1 * time.Second,
}

warnSampler := &zerolog.BurstSampler{
Burst: 3,
Period: 1 * time.Second,
// Log every 5th message after exceeding the burst rate of 3 messages per
// second
NextSampler: &zerolog.BasicSampler{N: 5},
}

errorSampler := &zerolog.BasicSampler{N: 2}

l := zerolog.New(os.Stdout).
With().
Timestamp().
Logger().
Sample(zerolog.LevelSampler{
WarnSampler: warnSampler,
InfoSampler: infoSampler,
ErrorSampler: errorSampler,
})

for i := 1; i <= 10; i++ {
l.Info().Msgf("a message from the gods: %d", i)
l.Warn().Msgf("warn message: %d", i)
l.Error().Msgf("error message: %d", i)
}
}

在此方案中,为 INFOWARNERROR 级别定义了不同的采样策略,而其他级别则正常记录。infoSampler 是我们在上一个代码片段中使用的示例配置(每秒记录 3 条消息并丢弃所有其他消息),而 warnSampler 使用该 NextSampler 属性在超过突发速率后记录每 5 条消息。errorSampler 使用 BasicSampler 策略,并记录每秒一条日志消息。此配置产生以下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
{"level":"info","time":"2023-09-02T08:40:58+01:00","message":"a message from the gods: 1"}
{"level":"warn","time":"2023-09-02T08:40:58+01:00","message":"warn message: 1"}
{"level":"error","time":"2023-09-02T08:40:58+01:00","message":"error message: 1"}
{"level":"info","time":"2023-09-02T08:40:58+01:00","message":"a message from the gods: 2"}
{"level":"warn","time":"2023-09-02T08:40:58+01:00","message":"warn message: 2"}
{"level":"info","time":"2023-09-02T08:40:58+01:00","message":"a message from the gods: 3"}
{"level":"warn","time":"2023-09-02T08:40:58+01:00","message":"warn message: 3"}
{"level":"error","time":"2023-09-02T08:40:58+01:00","message":"error message: 3"}
{"level":"warn","time":"2023-09-02T08:40:58+01:00","message":"warn message: 4"}
{"level":"error","time":"2023-09-02T08:40:58+01:00","message":"error message: 5"}
{"level":"error","time":"2023-09-02T08:40:58+01:00","message":"error message: 7"}
{"level":"warn","time":"2023-09-02T08:40:58+01:00","message":"warn message: 9"}
{"level":"error","time":"2023-09-02T08:40:58+01:00","message":"error message: 9"}

请注意消息的记录方式 (在一秒内生成的所有其他 INFO 消息将被丢弃), WARN 消息在突发速率之后记录三次, 并根据 NextSampler 属性中定义的策略再记录两次。最后,ERROR 消息被记录五次,而不是十次(1/2)。

Zerolog 还提供了全局 DisableSampling() 方法,用于在所有 Logger 中启用或禁用所有形式的采样。它采用一个布尔参数来控制是启用还是禁用日志采样,当您需要动态更新配置以响应某些事件时,这可能很有帮助。

Zerolog 中的钩子

Zerolog 提供了一种通过 Hook 接口挂接到日志记录过程的方法,定义如下:

1
2
3
4
type Hook interface {
// Run runs the hook with the event.
Run(e *zerolog.Event, level zerolog.Level, message string)
}

在具体类型上实现 Hook 接口时,可以使用 Logger.Hook() 方法将其应用于 Logger,以便在每次记录日志时执行其 Run() 方法。然后,您可以根据事件的日志级别或一些其他条件运行不同的操作。

下面是一个示例,该示例将记录在 ERROR 级别或更高级别的消息发送到 Telegram 频道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package main

import (
"context"
"os"
"time"

"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/telegram"
"github.com/rs/zerolog"
)

var wg sync.WaitGroup

type TelegramHook struct{}

func (t *TelegramHook) Run(
e *zerolog.Event,
level zerolog.Level,
message string,
) {
if level > zerolog.WarnLevel {
wg.Add(1)
go func() {
_ = notifyTelegram("", message)
wg.Done()
}()
}
}

func notifyTelegram(title, msg string) error {
telegramService, err := telegram.New(
"<telegram bot token>",
)
if err != nil {
return err
}

telegramService.AddReceivers("<chat id>")

notifier := notify.New()

notifier.UseServices(telegramService)

ctx, cancel := context.WithTimeout(
context.Background(),
30*time.Second,
)

defer cancel()

return notifier.Send(ctx, title, msg)
}

func main() {
logger := zerolog.New(os.Stdout).
Level(zerolog.TraceLevel).
With().
Timestamp().
Logger()

logger = logger.Hook(&TelegramHook{})

logger.Trace().Msg("trace message")
logger.Debug().Msg("debug message")
logger.Info().Msg("info message")
logger.Warn().Msg("warn message")
logger.Error().Msg("error message")
logger.WithLevel(zerolog.FatalLevel).Msg("fatal message")
logger.WithLevel(zerolog.PanicLevel).Msg("panic message")

wg.Wait()
}

上面的程序创建了一个 TelegramHook 实现接口的 zerolog.Hook 类型。它的 Run() 方法检查所记录消息的级别,如果它比 WARN 级别更严重,则将其发送到 Telegram 频道。如果运行该程序(在替换上面突出显示的占位符之后),您将观察到每条日志消息都打印到控制台,并且 ERRORFATALPANIC 日志也会发送到配置的 Telegram 通道。

使用 Zerolog 记录错误

使用 Zerolog 记录错误的最简单方法是在 ERROR 级别进行记录,并在 Err() 生成的 zerolog.Event。这会将一个 error 属性添加到日志条目中,其中包含相关错误的详细信息:

1
2
3
logger.Error().
Err(errors.New("file open failed")).
Msg("something happened!")

输出:

1
{"level":"error","error":"file open failed","time":"2023-08-31T22:59:07+01:00","message":"something happened!"}

您可以通过更改以下 zerolog.ErrorFieldName 的值将错误的字段名称更改为其他值:

1
2
3
4
5
zerolog.ErrorFieldName = "err"

logger.Error().
Err(errors.New("file open failed")).
Msg("something happened!")

输出:

1
{"level":"error","err":"file open failed","time":"2023-08-31T22:59:07+01:00","message":"something happened!"}

虽然上面的输出提供了有关所发生错误的详细信息,但它不显示导致错误的代码执行路径,这对于调试问题至关重要。您可以通过 Event 上的 Stack() 方法在错误日志中包含堆栈跟踪来解决此问题,但在它生效之前,必须分配给 zerolog.ErrorStackMarshaler 可以从错误中提取堆栈跟踪的函数。您可以将 pkg/errorszerolog/pkgerrors 帮助程序结合使用,以将堆栈跟踪添加到错误日志中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"os"

"github.com/pkg/errors"

"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)

func main() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack

logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
logger.Error().
Stack().
Err(errors.New("file open failed!")).
Msg("something happened!")
}

输出:

1
{"level":"error","stack":[{"func":"main","line":"19","source":"main.go"},{"func":"main","line":"250","source":"proc.go"},{"func":"goexit","line":"1594","source":"asm_amd64.s"}],"error":"file open failed!","time":"2023-08-24T21:52:24+01:00","message":"something happened!"}

请注意,该 stack 属性如何包含一个 JSON 格式的堆栈跟踪,该跟踪描述导致错误的程序执行。在调查应用程序中的意外错误时,此信息可能非常宝贵。

还可以使用 FATALPANIC 级别来记录应用程序无法恢复的特别严重的错误。请注意,FATAL 级别在进行日志记录会导致程序立即退出,退出状态为 1。而 PANIC 级别将调用 panic()

1
2
err := errors.New("failed to connect to database")
logger.Fatal().Err(err).Msg("something catastrophic happened!")

输出:

1
2
{"level":"fatal","error":"failed to connect to database","time":"2023-09-01T09:34:49+01:00","message":"something catastrophic happened!"}
exit status 1

如果要在不调用 panic 或者 os.Exit(1) 的情况下记录 FATAL 或者 PANIC 级别的消息,则必须使用如下所示 WithLevel() 的方法:

1
2
3
4
err := errors.New("failed to connect to database")
logger.WithLevel(zerolog.FatalLevel).
Err(err).
Msg("something catastrophic happened!")

程序将不再立即退出,但事件仍记录在适当的级别:

1
{"level":"fatal","error":"failed to connect to database","time":"2023-09-01T09:35:27+01:00","message":"something catastrophic happened!"}

记录到文件

使用 Zerolog 写入日志文件的选项与使用标准库日志包时的选项几乎相同。由于可以将实现 io.Writer 接口的类型传递给 zerolog.New() 方法,因此只要使用适当的权限打开文件,任何 os.File 实例都将按预期工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"os"

"github.com/rs/zerolog"
)

func main() {
file, err := os.OpenFile(
"myapp.log",
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
0664,
)
if err != nil {
panic(err)
}

defer file.Close()

logger := zerolog.New(file).With().Timestamp().Logger()

logger.Info().Msg("Info message")
}

执行上述程序后,您会注意到当前目录中存在一个 myapp.log 文件。您可以查看其内容,以验证登录文件是否按预期工作:

1
cat myapp.log

输出:

1
{"level":"info","time":"2023-08-27T11:38:27+01:00","message":"Info message"}

在 Web 应用程序中使用 Zerolog

我们可以通过下面的方式来定义一个 logger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package logger

import (
"io"
"os"
"runtime/debug"
"strconv"
"sync"
"time"

"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
"gopkg.in/natefinch/lumberjack.v2"
)

var once sync.Once

var log zerolog.Logger

func Get() zerolog.Logger {
once.Do(func() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.TimeFieldFormat = time.RFC3339Nano

logLevel, err := strconv.Atoi(os.Getenv("LOG_LEVEL"))
if err != nil {
logLevel = int(zerolog.InfoLevel) // 默认为 INFO
}

var output io.Writer = zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: time.RFC3339,
}

if os.Getenv("APP_ENV") != "development" {
fileLogger := &lumberjack.Logger{
Filename: "demo.log",
MaxSize: 5,
MaxBackups: 10,
MaxAge: 14,
Compress: true,
}

output = zerolog.MultiLevelWriter(os.Stderr, fileLogger)
}

var gitRevision string

buildInfo, ok := debug.ReadBuildInfo()
if ok {
for _, v := range buildInfo.Settings {
if v.Key == "vcs.revision" {
gitRevision = v.Value
break
}
}
}

log = zerolog.New(output).
Level(zerolog.Level(logLevel)).
With().
Timestamp().
Str("git_revision", gitRevision).
Str("go_version", buildInfo.GoVersion).
Logger()
})

return log
}

在此文件中,Get() 函数返回一个 zerolog.Logger 实例,该实例已根据 LOG_LEVEL 环境变量(如果存在且是有效数字)配置了最小级别,否则默认为 INFO 级别。 APP_ENV 环境变量还用于指定 ConsoleWriter API 应用于仅在开发环境中美化日志输出(发送到标准错误)。在其他环境(如暂存或生产)中,通过使用 lumberjack 库和 zerolog.MultiLevelWriter() 方法,日志被记录为标准错误和 demo.log 滚动日志文件。

请注意,Logger 初始化是在 once.Do() 方法的函数参数中完成的,因此无论调用多少次 Get() ,它都只初始化一次。

接着,我们就可以在我们的应用程序中使用这个 logger 了:

1
2
l := logger.Get()
l.Fatal().Err(err).Msg("test message")

创建日志记录中间件

现在我们的 Logger 实例已经准备就绪,我们可以创建一个中间件函数,将所有传入的 HTTP 请求记录到服务器。我们将使用它向每个日志条目添加上下文信息,例如请求数据、响应代码等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

l := logger.Get()

next.ServeHTTP(w, r)

l.
Info().
Str("method", r.Method).
Str("url", r.URL.RequestURI()).
Str("user_agent", r.UserAgent()).
Dur("elapsed_ms", time.Since(start)).
Msg("incoming request")
})
}

requestLogger() 函数返回一个 HTTP 处理程序,该处理程序记录有关 HTTP 请求的多个详细信息,例如请求 URL、HTTP 方法、客户端用户代理以及完成请求所花费的时间。您还可以 defer 记录调用,以确保如果处理程序发生崩溃,仍会记录请求,以便找出导致崩溃的原因。