零分配 JSON 日志记录器
zerolog 包提供了一个快速简单的专用于 JSON 输出的日志记录器。
Zerolog 的 API 旨在提供出色的开发者体验和惊人的性能。其独特的链式 API 使 zerolog 能够通过避免分配和反射来编写 JSON(或 CBOR)日志事件。
Uber 的 zap 库首创了这种方法。Zerolog 在此基础上更进一步,提供了更简单易用的 API 和更好的性能。
为了保持代码库和 API 的简洁,zerolog 只专注于高效的结构化日志记录。通过提供的(但效率较低的)zerolog.ConsoleWriter
可以实现控制台上的美化日志输出。
谁在使用 zerolog
了解谁在使用 zerolog并将你的公司/项目添加到列表中。
特性
- 极快的速度
- 低至零内存分配
- 分级日志记录
- 日志采样
- 钩子
- 上下文字段
context.Context
集成- 与
net/http
集成 - JSON 和 CBOR 编码格式
- 开发时的美化日志输出
- 错误日志记录(可选堆栈追踪)
安装
go get -u github.com/rs/zerolog/log
入门
简单日志记录示例
对于简单的日志记录,导入全局日志记录包 github.com/rs/zerolog/log
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// UNIX 时间比大多数时间戳更快且更小
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Print("hello world")
}
// 输出: {"time":1516134303,"level":"debug","message":"hello world"}
注意:默认情况下,日志写入
os.Stderr
注意:log.Print
的默认日志级别是 trace
上下文日志记录
zerolog 允许以键值对的形式向日志消息添加数据。添加到消息中的数据为日志事件提供了"上下文",这对调试以及众多其他用途至关重要。以下是一个示例:
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Debug().
Str("Scale", "833 cents").
Float64("Interval", 833.09).
Msg("Fibonacci is everywhere")
log.Debug().
Str("Name", "Tom").
Send()
}
// 输出: {"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"}
// 输出: {"level":"debug","Name":"Tom","time":1562212768}
你会注意到在上面的示例中,添加上下文字段时,字段是强类型的。你可以在这里找到支持的字段的完整列表
分级日志记录
简单分级日志记录示例
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Info().Msg("hello world")
}
// 输出: {"time":1516134303,"level":"info","message":"hello world"}
非常重要的是要注意,当使用 zerolog 链式 API 时,如上所示(
log.Info().Msg("hello world")
),链必须包含Msg
或Msgf
方法调用。如果你忘记添加这两个方法中的任何一个,日志将不会记录,并且没有编译时错误来提醒你这一点。
zerolog 允许在以下级别(从高到低)进行日志记录:
- panic (
zerolog.PanicLevel
, 5) - fatal (
zerolog.FatalLevel
, 4) - error (
zerolog.ErrorLevel
, 3) - warn (
zerolog.WarnLevel
, 2) - info (
zerolog.InfoLevel
, 1) - debug (
zerolog.DebugLevel
, 0) - trace (
zerolog.TraceLevel
, -1)
你可以使用 zerolog 包中的 SetGlobalLevel
函数将全局日志级别设置为上述任何选项,传入给定的常量之一,例如 zerolog.InfoLevel
将是"info"级别。无论选择哪个级别,所有级别大于或等于该级别的日志都将被记录。要完全关闭日志记录,请传递 zerolog.Disabled
常量。
设置全局日志级别
这个示例使用命令行标志来演示根据所选日志级别的不同输出。
package main
import (
"flag"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
debug := flag.Bool("debug", false, "sets log level to debug")
flag.Parse()
// 本示例的默认级别为 info,除非存在 debug 标志
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if *debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
log.Debug().Msg("This message appears only when log level set to Debug")
log.Info().Msg("This message appears when log level set to Debug or Info")
if e := log.Debug(); e.Enabled() {
// 仅在启用时计算日志输出。
value := "bar"
e.Str("foo", value).Msg("some debug message")
}
}
Info 输出(无标志)
$ ./logLevelExample
{"time":1516387492,"level":"info","message":"This message appears when log level set to Debug or Info"}
Debug 输出(设置 debug 标志)
$ ./logLevelExample -debug
{"time":1516387573,"level":"debug","message":"This message appears only when log level set to Debug"}
{"time":1516387573,"level":"info","message":"This message appears when log level set to Debug or Info"}
{"time":1516387573,"level":"debug","foo":"bar","message":"some debug message"}
不带级别或消息的日志记录
你可以选择使用Log
方法进行无特定级别的日志记录。你也可以通过在Msg
方法的msg string
参数中设置空字符串来不写入消息。以下示例演示了这两种方式。
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Log().
Str("foo", "bar").
Msg("")
}
// 输出: {"time":1494567715,"foo":"bar"}
错误日志记录
你可以使用Err
方法记录错误
package main
import (
"errors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
err := errors.New("似乎这里有个错误")
log.Error().Err(err).Msg("")
}
// 输出: {"level":"error","error":"似乎这里有个错误","time":1609085256}
错误的默认字段名是
error
,你可以通过设置zerolog.ErrorFieldName
来满足你的需求。
带堆栈跟踪的错误日志记录
使用github.com/pkg/errors
,你可以为错误添加格式化的堆栈跟踪。
package main
import (
"github.com/pkg/errors"
"github.com/rs/zerolog/pkgerrors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
err := outer()
log.Error().Stack().Err(err).Msg("")
}
func inner() error {
return errors.New("似乎这里有个错误")
}
func middle() error {
err := inner()
if err != nil {
return err
}
return nil
}
func outer() error {
err := middle()
if err != nil {
return err
}
return nil
}
// 输出: {"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}],"error":"似乎这里有个错误","time":1609086683}
必须设置zerolog.ErrorStackMarshaler才能输出堆栈信息。
记录致命错误消息
package main
import (
"errors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
err := errors.New("收债人的一生都在应对紧张局势")
service := "myservice"
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Fatal().
Err(err).
Str("service", service).
Msgf("无法启动%s", service)
}
// 输出: {"time":1516133263,"level":"fatal","error":"收债人的一生都在应对紧张局势","service":"myservice","message":"无法启动myservice"}
// exit status 1
注意:即使日志记录器被禁用,使用
Msgf
也会产生一次内存分配。
创建日志记录器实例以管理不同的输出
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().Str("foo", "bar").Msg("你好,世界")
// 输出: {"level":"info","time":1494567715,"message":"你好,世界","foo":"bar"}
子日志记录器让你可以链式添加额外上下文
sublogger := log.With().
Str("component", "foo").
Logger()
sublogger.Info().Msg("你好,世界")
// 输出: {"level":"info","time":1494567715,"message":"你好,世界","component":"foo"}
美化日志输出
要输出人类友好的、带颜色的日志,使用zerolog.ConsoleWriter
:
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
log.Info().Str("foo", "bar").Msg("你好,世界")
// 输出: 3:04PM INF 你好,世界 foo=bar
自定义配置和格式化:
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
output.FormatLevel = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
}
output.FormatMessage = func(i interface{}) string {
return fmt.Sprintf("***%s****", i)
}
output.FormatFieldName = func(i interface{}) string {
return fmt.Sprintf("%s:", i)
}
output.FormatFieldValue = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%s", i))
}
log := zerolog.New(output).With().Timestamp().Logger()
log.Info().Str("foo", "bar").Msg("你好,世界")
// 输出: 2006-01-02T15:04:05Z07:00 | INFO | ***你好,世界**** foo:BAR
子字典
log.Info().
Str("foo", "bar").
Dict("dict", zerolog.Dict().
Str("bar", "baz").
Int("n", 1),
).Msg("你好,世界")
// 输出: {"level":"info","time":1494567715,"foo":"bar","dict":{"bar":"baz","n":1},"message":"你好,世界"}
自定义自动字段名
zerolog.TimestampFieldName = "t"
zerolog.LevelFieldName = "l"
zerolog.MessageFieldName = "m"
log.Info().Msg("你好,世界")
// 输出: {"l":"info","t":1494567715,"m":"你好,世界"}
为全局日志记录器添加上下文字段
log.Logger = log.With().Str("foo", "bar").Logger()
在日志中添加文件和行号
等同于Llongfile
:
log.Logger = log.With().Caller().Logger()
log.Info().Msg("你好,世界")
// 输出: {"level": "info", "message": "你好,世界", "caller": "/go/src/your_project/some_file:21"}
等同于Lshortfile
:
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
return filepath.Base(file) + ":" + strconv.Itoa(line)
}
log.Logger = log.With().Caller().Logger()
log.Info().Msg("你好,世界")
// 输出: {"level": "info", "message": "你好,世界", "caller": "some_file:21"}
线程安全、无锁、非阻塞的写入器
如果你的写入器可能很慢或不是线程安全的,而你需要确保日志生产者永远不会被慢速写入器拖慢,你可以使用diode.Writer
,如下所示:
wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
fmt.Printf("日志记录器丢弃了 %d 条消息", missed)
})
log := zerolog.New(wr)
log.Print("测试")
要使用此功能,你需要安装code.cloudfoundry.org/go-diodes
。
日志采样
sampled := log.Sample(&zerolog.BasicSampler{N: 10})
sampled.Info().Msg("每10条消息记录一次")
// 输出: {"time":1494567715,"level":"info","message":"每10条消息记录一次"}
更高级的采样:
// 每1秒期间允许5条debug消息。
// 超过5条debug消息后,每100条debug消息记录1条。
// 其他级别不进行采样。
sampled := log.Sample(zerolog.LevelSampler{
DebugSampler: &zerolog.BurstSampler{
Burst: 5,
Period: 1*time.Second,
NextSampler: &zerolog.BasicSampler{N: 100},
},
})
sampled.Debug().Msg("你好,世界")
// 输出: {"time":1494567715,"level":"debug","message":"hello world"}
### 钩子
```go
type SeverityHook struct{}
func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
if level != zerolog.NoLevel {
e.Str("severity", level.String())
}
}
hooked := log.Hook(SeverityHook{})
hooked.Warn().Msg("")
// 输出: {"level":"warn","severity":"warn"}
通过上下文传递子日志记录器
ctx := log.With().Str("component", "module").Logger().WithContext(ctx)
log.Ctx(ctx).Info().Msg("hello world")
// 输出: {"component":"module","level":"info","message":"hello world"}
设置为标准日志输出
log := zerolog.New(os.Stdout).With().
Str("foo", "bar").
Logger()
stdlog.SetFlags(0)
stdlog.SetOutput(log)
stdlog.Print("hello world")
// 输出: {"foo":"bar","message":"hello world"}
context.Context 集成
Go 上下文通常在整个 Go 代码中传递,这可以帮助你将 Logger 传递到可能难以注入的地方。可以使用 Logger.WithContext(ctx)
将 Logger
实例附加到 Go 上下文(context.Context
)中,并使用 zerolog.Ctx(ctx)
从中提取。例如:
func f() {
logger := zerolog.New(os.Stdout)
ctx := context.Background()
// 将 Logger 附加到 context.Context
ctx = logger.WithContext(ctx)
someFunc(ctx)
}
func someFunc(ctx context.Context) {
// 从 Go Context 获取 Logger。如果为 nil,则返回
// `zerolog.DefaultContextLogger`,如果
// `DefaultContextLogger` 为 nil,则返回一个禁用的日志记录器。
logger := zerolog.Ctx(ctx)
logger.Info().Msg("Hello")
}
第二种形式的 context.Context
集成允许你将当前的 context.Context 传递到记录的事件中,并从钩子中检索它。这对于记录存储在 Go 上下文中的跟踪和跨度 ID 或其他信息非常有用,并有助于在某些系统中统一日志记录和跟踪:
type TracingHook struct{}
func (h TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
ctx := e.GetCtx()
spanId := getSpanIdFromContext(ctx) // 根据你的跟踪框架
e.Str("span-id", spanId)
}
func f() {
// 设置日志记录器
logger := zerolog.New(os.Stdout)
logger = logger.Hook(TracingHook{})
ctx := context.Background()
// 使用 Ctx 函数使上下文对钩子可用
logger.Info().Ctx(ctx).Msg("Hello")
}
与 net/http
集成
github.com/rs/zerolog/hlog
包提供了一些辅助函数,用于将 zerolog 与 http.Handler
集成。
在这个例子中,我们使用 alice 来安装日志记录器,以提高可读性。
log := zerolog.New(os.Stdout).With().
Timestamp().
Str("role", "my-service").
Str("host", host).
Logger()
c := alice.New()
// 安装日志处理器,默认输出到控制台
c = c.Append(hlog.NewHandler(log))
// 安装一些提供的额外处理器来设置请求的上下文字段。
// 通过这个处理器,我们所有的日志都会带有一些预填充的字段。
c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
hlog.FromRequest(r).Info().
Str("method", r.Method).
Stringer("url", r.URL).
Int("status", status).
Int("size", size).
Dur("duration", duration).
Msg("")
}))
c = c.Append(hlog.RemoteAddrHandler("ip"))
c = c.Append(hlog.UserAgentHandler("user_agent"))
c = c.Append(hlog.RefererHandler("referer"))
c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id"))
// 这是你最终的处理器
h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求的上下文中获取日志记录器。你可以安全地假设它
// 总是存在的:如果处理器被移除,hlog.FromRequest
// 将返回一个无操作的日志记录器。
hlog.FromRequest(r).Info().
Str("user", "current user").
Str("status", "ok").
Msg("Something happened")
// 输出: {"level":"info","time":"2001-02-03T04:05:06Z","role":"my-service","host":"local-hostname","req_id":"b4g0l5t6tfid6dtrapu0","user":"current user","status":"ok","message":"Something happened"}
}))
http.Handle("/", h)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal().Err(err).Msg("Startup failed")
}
多重日志输出
可以使用 zerolog.MultiLevelWriter
将日志消息发送到多个输出。
在这个例子中,我们将日志消息同时发送到 os.Stdout
和内置的 ConsoleWriter。
func main() {
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
multi := zerolog.MultiLevelWriter(consoleWriter, os.Stdout)
logger := zerolog.New(multi).With().Timestamp().Logger()
logger.Info().Msg("Hello World!")
}
// 输出 (第1行: 控制台; 第2行: Stdout)
// 12:36PM INF Hello World!
// {"level":"info","time":"2019-11-07T12:36:38+03:00","message":"Hello World!"}
全局设置
可以更改一些设置,这些设置将应用于所有日志记录器:
log.Logger
:你可以设置这个值来自定义全局日志记录器(用于包级别方法的日志记录器)。zerolog.SetGlobalLevel
:可以提高所有日志记录器的最低级别。使用zerolog.Disabled
调用此函数可以完全禁用日志记录(静默模式)。zerolog.DisableSampling
:如果参数为true
,所有采样日志记录器将停止采样并发出 100% 的日志事件。zerolog.TimestampFieldName
:可以设置为自定义Timestamp
字段名。zerolog.LevelFieldName
:可以设置为自定义级别字段名。zerolog.MessageFieldName
:可以设置为自定义消息字段名。zerolog.ErrorFieldName
:可以设置为自定义Err
字段名。zerolog.TimeFieldFormat
:可以设置为自定义Time
字段值的格式。如果设置为zerolog.TimeFormatUnix
、zerolog.TimeFormatUnixMs
或zerolog.TimeFormatUnixMicro
,时间将格式化为 UNIX 时间戳。zerolog.DurationFieldUnit
:可以设置为自定义由Dur
添加的 time.Duration 类型字段的单位(默认:time.Millisecond
)。zerolog.DurationFieldInteger
:如果设置为true
,Dur
字段将格式化为整数而不是浮点数(默认:false
)。zerolog.ErrorHandler
:当 zerolog 无法在其输出上写入事件时调用。如果未设置,错误将打印到标准错误输出。此处理程序必须是线程安全的且非阻塞的。zerolog.FloatingPointPrecision
:如果设置为非 -1 的值,控制在 JSON 中格式化浮点数时的位数。有关更多详细信息,请参见 strconv.FormatFloat。
字段类型
标准类型
Str
Bool
Int
、Int8
、Int16
、Int32
、Int64
Uint
、Uint8
、Uint16
、Uint32
、Uint64
Float32
、Float64
高级字段
Err
:接受一个error
并使用zerolog.ErrorFieldName
字段名将其渲染为字符串。Func
:仅在启用该级别时运行func
。Timestamp
:插入一个使用zerolog.TimestampFieldName
字段名的时间戳字段,格式化使用zerolog.TimeFieldFormat
。Time
:添加一个使用zerolog.TimeFieldFormat
格式化的时间字段。Dur
:添加一个time.Duration
字段。Dict
:作为事件的字段添加一个子键/值。RawJSON
:添加一个已编码JSON([]byte
)的字段。Hex
:添加一个格式化为十六进制字符串的值字段([]byte
)。Interface
:使用反射来序列化类型。
大多数字段也以切片格式提供(Strs
用于[]string
,Errs
用于[]error
等)。
二进制编码
除了默认的JSON编码外,zerolog
还可以使用CBOR编码生成二进制日志。编码选择可以在编译时使用构建标签binary_log
来决定,如下所示:
go build -tags binary_log .
要解码二进制编码的日志文件,可以使用任何CBOR解码器。经测试可与zerolog库一起使用的解码器是CSD。
相关项目
- grpc-zerolog:使用
zerolog
实现的grpclog.LoggerV2
接口 - overlog:使用
zerolog
实现的Mapped Diagnostic Context
接口 - zerologr:使用
zerolog
实现的logr.LogSink
接口
基准测试
更全面和最新的基准测试请参见logbench。
所有操作都是无分配的(这些数字包括JSON编码):
BenchmarkLogEmpty-8 100000000 19.1 ns/op 0 B/op 0 allocs/op
BenchmarkDisabled-8 500000000 4.07 ns/op 0 B/op 0 allocs/op
BenchmarkInfo-8 30000000 42.5 ns/op 0 B/op 0 allocs/op
BenchmarkContextFields-8 30000000 44.9 ns/op 0 B/op 0 allocs/op
BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/op
有几个包含zerolog的Go日志基准测试和比较。
使用Uber的zap比较基准:
记录一条消息和10个字段:
库 | 时间 | 分配字节数 | 分配对象数 |
---|---|---|---|
zerolog | 767 ns/op | 552 B/op | 6 allocs/op |
:zap: zap | 848 ns/op | 704 B/op | 2 allocs/op |
:zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op |
go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op |
lion | 5392 ns/op | 5807 B/op | 63 allocs/op |
logrus | 5661 ns/op | 6092 B/op | 78 allocs/op |
apex/log | 15332 ns/op | 3832 B/op | 65 allocs/op |
log15 | 20657 ns/op | 5632 B/op | 93 allocs/op |
使用已有10个上下文字段的logger记录一条消息:
库 | 时间 | 分配字节数 | 分配对象数 |
---|---|---|---|
zerolog | 52 ns/op | 0 B/op | 0 allocs/op |
:zap: zap | 283 ns/op | 0 B/op | 0 allocs/op |
:zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
lion | 2702 ns/op | 4074 B/op | 38 allocs/op |
go-kit | 3378 ns/op | 3046 B/op | 52 allocs/op |
logrus | 4309 ns/op | 4564 B/op | 63 allocs/op |
apex/log | 13456 ns/op | 2898 B/op | 51 allocs/op |
log15 | 14179 ns/op | 2642 B/op | 44 allocs/op |
记录一个静态字符串,不带任何上下文或printf
风格的模板:
库 | 时间 | 分配字节数 | 分配对象数 |
---|---|---|---|
zerolog | 50 ns/op | 0 B/op | 0 allocs/op |
:zap: zap | 236 ns/op | 0 B/op | 0 allocs/op |
标准库 | 453 ns/op | 80 B/op | 2 allocs/op |
:zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
go-kit | 508 ns/op | 656 B/op | 13 allocs/op |
lion | 771 ns/op | 1224 B/op | 10 allocs/op |
logrus | 1244 ns/op | 1505 B/op | 27 allocs/op |
apex/log | 2751 ns/op | 584 B/op | 11 allocs/op |
log15 | 5181 ns/op | 1592 B/op | 26 allocs/op |
注意事项
字段重复
请注意,zerolog不会对字段进行去重。多次使用相同的键会在最终的JSON中创建多个键:
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().
Timestamp().
Msg("dup")
// 输出: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
在这种情况下,许多消费者会取最后一个值,但这并不能保证;如有疑问,请检查您的消费者。
并发安全
使用UpdateContext时要小心。它不是并发安全的。使用With方法创建一个子logger:
func handler(w http.ResponseWriter, r *http.Request) {
// 创建一个子logger以确保并发安全
logger := log.Logger.With().Logger()
// 添加上下文字段,例如来自HTTP头的User-Agent
logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
...
})
}