前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。
前言
Zerolog
是一个高性能、零内存分配的 Go 日志库。它为不需要垃圾回收的延迟敏感型应用程序提供结构化日志记录功能。您可以以完全零分配的方式使用,这样在初始化记录器对象后,堆上不会再分配其他对象,从而防止触发垃圾回收。
本教程将解释如何在 Go 应用程序中安装、设置和使用 Zerolog 记录器。我们将首先介绍它的 API 和它提供的所有选项,并展示如何以各种方式自定义它们。最后,我们将描述如何在典型的 Web 应用程序中使用它,以便您可以很好地了解如何在您的项目中采用它。
开始使用 Zerolog
我们可以通过下面的命令安装 Zerolog 到我们的 Go 项目中:
go get -u github.com/rs/zerolog/log
该库提供了一个预配置且全局可用的 logger
,您可以通过 zerolog/log
软件包在任何文件中导入和使用该 logger
:
package main
import (
"github.com/rs/zerolog/log"
)
func main() {
log.Info().Msg("Hello from Zerolog global logger")
}
上述程序将 JSON 格式的日志条目输出到控制台:
{"level":"info","time":"2023-08-18T13:45:51+01:00","message":"Hello from global logger"}
默认情况下,全局 logger
打印为标准错误,并且它还配置为在级别 TRACE
上记录,尽管您可以通过调用主 zerolog
包提供的 SetGlobalLevel()
函数来设置不同的最小级别:
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")
}
{"level":"error","time":"2023-08-18T13:51:44+01:00","message":"Error message"}
除了 JSON 日志记录外,您还可以配置 Zerolog
以输出以 CBOR
格式编码的二进制日志。您可以在编译应用程序时使用 binary_log
build
标记来启用它:
go build -tags binary_log .
执行生成的二进制文件时,您将看到如下所示的输出:
�eleveleerrordtime��A�lׂ�p�gmessagemError message�%
注意:不是我的文章乱码,而是执行生成的二进制文件输出的就是这样的乱码。这是因为它是二进制的格式的,不是 ASCII 格式的。
您可以使用任何 CBOR
解码器(例如 csd
)将此二进制日志条目解码为 JSON:
./main 2> >(csd)
输出:
{"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
:
package main
import (
"os"
"github.com/rs/zerolog"
)
func main() {
logger := zerolog.New(os.Stdout)
logger.Trace().Msg("Trace message")
}
输出:
{"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
包上提供的级别常量(TraceLevel
,DebugLevel
,等):
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
。
logger.Info().
Str("name", "john").
Int("age", 22).
Bool("registered", true).
Msg("new signup!")
输出:
{"level":"info","name":"john","age":22,"registered":true,"message":"new signup!"}
如果要省略该 message
字段,可以调用 Msg()
或改用 Send()
来代替(不传参数):
logger.Info().
Str("name", "john").
Int("age", 22).
Bool("registered", true).
Send()
输出:
{"level":"info","name":"john","age":22,"registered":true}
请注意,必须始终在 zerolog.Event
上最后调用一个事件关闭方法,以便记录相应的条目。如果未使用这些方法,则不会记录日志条目。
logger.Info().
Str("name", "john").
Int("age", 22).
Bool("registered", true)
这不会有任何日志记录。
此外,除了要记得调用事件关闭方法外,也不要在一个 event
对象上调用多次事件关闭方法。
向 logger 添加全局上下文
在上一节中,您了解了如何使用 zerolog.Event
类型上的方法将相关上下文添加到日志条目。本节将更进一步,向您展示如何将上下文数据添加到 Logger 其本身,以确保此类数据包含在记录器生成的所有后续记录中。
有两种主要方法可以向 Zerolog Logger
添加上下文。第一种涉及使用返回 zerolog With()
的方法。上下文实例,允许您通过与 zerolog.Event
类型类似的字段方法向键值对中的记录器添加其他属性。然后,在将必要的数据添加到上下文后,必须调用该 Logger()
方法以返回具有更新上下文的新 Logger
对象。
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()
}
输出日志:
{"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
字段添加到 生成的所有记录中,这很有意义,因为所有日志记录都应包含时间戳。您还可以将文件和行号添加到所有日志条目中,如下所示:
func main() {
logger := zerolog.New(os.Stdout).With().Timestamp().Caller().Logger()
logger.Info().Msg("info message")
logger.Debug().Str("username", "joshua").Send()
}
输出日志:
{"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
,以将它们与其他记录区分开来。下面是一个人为的例子,演示了这是如何工作的:
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")
}
输出:
{"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
的内部上下文(无需创建副本),您可以像这样使用它:
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")
}
输出日志:
{"level":"info","name":"john","time":"2023-09-01T09:25:20+01:00","message":"info message"}
在开发中美化日志
在开发环境中,您可能会发现以更易于阅读的格式从应用程序输出日志条目会很有帮助,这样就可以轻松发现各种事件,而不会被不相关的符号和字段分散注意力。Zerolog
提供了一个 ConsoleWriter
类型,用于解析原始 JSON 条目,并将其以彩色格式输出到控制台。
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")
}
输出:
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
类型上提供的选项来自定义输出的外观和格式:
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,
},
}
输出:
[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
实际效果如下:
请注意,时间戳现在是如何从输出中排除的,以及日志级别、调用方信息和日志消息的格式有何不同。格式化的字段也不再着色,但您可以使用 pterm
或 gookit/color
等库来着色输出。
注意不要在生产环境中使用 , ConsoleWriter
因为它会大大减慢日志记录的速度。它只是为了帮助在开发应用程序时使日志更易于阅读。您可以使用环境变量仅在开发中启用 ConsoleWriter 输出:
var output io.Writer = zerolog.ConsoleWriter{...}
if os.Getenv("GO_ENV") != "development" {
output = os.Stderr
}
使用 Zerolog 进行日志采样
采样是一种用于有意删除重复日志条目的技术,以便只保留和处理其中的一部分。当您的高流量应用程序生成大量记录时,这很有帮助,并且存储每一条记录将导致过高的存储和处理成本,这可能是不可取的。采样通过防止每秒记录数百或数千次相同的日志来解决此问题,从而防止资源浪费。
以下是使用 Zerolog
对日志进行采样的最基本方法:
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 消息通常被记录十次,但由于采样,它只被记录两次:
{"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
可用于限制在一段时间内记录的日志数:
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 条:
{"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"}
您只能对特定级别进行抽样,如下所示:
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,
})
在这里,将仅对 INFO
和 WARN
日志进行采样,而其他日志将照常记录,从而产生以下输出:
{"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
等 。
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)
}
}
在此方案中,为 INFO
、 WARN
和 ERROR
级别定义了不同的采样策略,而其他级别则正常记录。infoSampler
是我们在上一个代码片段中使用的示例配置(每秒记录 3 条消息并丢弃所有其他消息),而 warnSampler
使用该 NextSampler
属性在超过突发速率后记录每 5 条消息。errorSampler
使用 BasicSampler
策略,并记录每秒一条日志消息。此配置产生以下结果:
{"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
接口挂接到日志记录过程的方法,定义如下:
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 频道:
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 频道。如果运行该程序(在替换上面突出显示的占位符之后),您将观察到每条日志消息都打印到控制台,并且 ERROR
、FATAL
和 PANIC
日志也会发送到配置的 Telegram 通道。
使用 Zerolog 记录错误
使用 Zerolog
记录错误的最简单方法是在 ERROR
级别进行记录,并在 Err()
生成的 zerolog.Event
。这会将一个 error
属性添加到日志条目中,其中包含相关错误的详细信息:
logger.Error().
Err(errors.New("file open failed")).
Msg("something happened!")
输出:
{"level":"error","error":"file open failed","time":"2023-08-31T22:59:07+01:00","message":"something happened!"}
您可以通过更改以下 zerolog.ErrorFieldName
的值将错误的字段名称更改为其他值:
zerolog.ErrorFieldName = "err"
logger.Error().
Err(errors.New("file open failed")).
Msg("something happened!")
输出:
{"level":"error","err":"file open failed","time":"2023-08-31T22:59:07+01:00","message":"something happened!"}
虽然上面的输出提供了有关所发生错误的详细信息,但它不显示导致错误的代码执行路径,这对于调试问题至关重要。您可以通过 Event
上的 Stack()
方法在错误日志中包含堆栈跟踪来解决此问题,但在它生效之前,必须分配给 zerolog.ErrorStackMarshaler
可以从错误中提取堆栈跟踪的函数。您可以将 pkg/errors
与 zerolog/pkgerrors
帮助程序结合使用,以将堆栈跟踪添加到错误日志中,如下所示:
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!")
}
输出:
{"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 格式的堆栈跟踪,该跟踪描述导致错误的程序执行。在调查应用程序中的意外错误时,此信息可能非常宝贵。
还可以使用 FATAL
或 PANIC
级别来记录应用程序无法恢复的特别严重的错误。请注意,FATAL
级别在进行日志记录会导致程序立即退出,退出状态为 1。而 PANIC
级别将调用 panic()
。
err := errors.New("failed to connect to database")
logger.Fatal().Err(err).Msg("something catastrophic happened!")
输出:
{"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()
的方法:
err := errors.New("failed to connect to database")
logger.WithLevel(zerolog.FatalLevel).
Err(err).
Msg("something catastrophic happened!")
程序将不再立即退出,但事件仍记录在适当的级别:
{"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
实例都将按预期工作。
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
文件。您可以查看其内容,以验证登录文件是否按预期工作:
cat myapp.log
输出:
{"level":"info","time":"2023-08-27T11:38:27+01:00","message":"Info message"}
在 Web 应用程序中使用 Zerolog
我们可以通过下面的方式来定义一个 logger
:
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
了:
l := logger.Get()
l.Fatal().Err(err).Msg("test message")
创建日志记录中间件
现在我们的 Logger
实例已经准备就绪,我们可以创建一个中间件函数,将所有传入的 HTTP 请求记录到服务器。我们将使用它向每个日志条目添加上下文信息,例如请求数据、响应代码等。
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 记录调用,以确保如果处理程序发生崩溃,仍会记录请求,以便找出导致崩溃的原因。