# Zero Allocation JSON Logger

Zerolog https://github.com/rs/zerolog

Zerolog 包提供了一个专用于 JSON 输出的快速且简单的 Logger 记录器。

Zerolog 的 API 旨在提供出色的开发人员体验和令人惊叹的性能。其独特的链接 API 允许 Zerolog 通过避免分配和反射来写入 JSON(或 CBOR)日志事件。

Uber 的 zap 库率先采用了这种方法。 Zerolog 通过更简单的 API 和更好的性能将这一概念提升到了一个新的水平。

为了保持代码库和 API 简单,zerolog 仅专注于高效的结构化日志记录。使用提供的(但效率低下) zerolog.ConsoleWriter 可以在 console 上进行漂亮的日志记录。

# 安装

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

# 日志

# 简单示例

package main
import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)
func main() {
    // UNIX Time is faster and smaller than most timestamps
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    log.Print("hello world")
}
// Output: {"time":1516134303,"level":"debug","message":"hello world"}

注意:默认情况下日志写入 os.Stderr 注意: log.Print 的默认日志级别是 debug

# 键值对日志

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()
}
// Output: {"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"}
// Output: {"level":"debug","Name":"Tom","time":1562212768}

# 键值对里加键值对日志

log.Info().
    Str("foo", "bar").
    Dict("dict", zerolog.Dict().
        Str("bar", "baz").
        Int("n", 1),
    ).Msg("hello world")
// Output: {"level":"info","time":1494567715,"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"}

# 日志分级

package main
import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)
func main() {
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    log.Info().Msg("hello world")
}
// Output: {"time":1516134303,"level":"info","message":"hello world"}

需要注意的是,当使用 Zerolog 链接 API 时,如上所示 ( log.Info().Msg("hello world" ),必须具有 MsgMsgf 方法调用。如果忘记添加其中任何一个,则不会出现日志,也不会出现编译时错误。

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)

# 没有级别或消息的日志

package main
import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)
func main() {
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    log.Log().
        Str("foo", "bar").
        Msg("")
}
// Output: {"time":1494567715,"foo":"bar"}

# 创建 Logger 实例

logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().Str("foo", "bar").Msg("hello world")
// Output: {"level":"info","time":1494567715,"message":"hello world","foo":"bar"}

# 子 Logger

sublogger := log.With().
                 Str("component", "foo").
                 Logger()
sublogger.Info().Msg("hello world")
// Output: {"level":"info","time":1494567715,"message":"hello world","component":"foo"}

# 美化日志

使用 zerolog.ConsoleWriter

log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
log.Info().Str("foo", "bar").Msg("Hello world")
// Output: 3:04PM INF Hello World 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("Hello World")
// Output: 2006-01-02T15:04:05Z07:00 | INFO  | ***Hello World**** foo:BAR

# 线程安全、无锁、非阻塞 writer

如果 writer 很慢,或者 writer 不是线程安全的。并且不想因为 writer 写入过慢减少日志的写入速度。试看看 diode.Writer ,如下所示:

wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
		fmt.Printf("Logger Dropped %d messages", missed)
	})
log := zerolog.New(wr)
log.Print("test")

需要安装 code.cloudfoundry.org/go-diodes 才能使用此功能。

# 日志采样

sampled := log.Sample(&zerolog.BasicSampler{N: 10})
sampled.Info().Msg("will be logged every 10 messages")
// Output: {"time":1494567715,"level":"info","message":"will be logged every 10 messages"}

采样进阶:

// 每 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("hello world")
// Output: {"time":1494567715,"level":"debug","message":"hello world"}

# Hooks

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("")
// Output: {"level":"warn","severity":"warn"}

# 通过 context 传递日志

ctx := log.With().Str("component", "module").Logger().WithContext(ctx)
log.Ctx(ctx).Info().Msg("hello world")
// Output: {"component":"module","level":"info","message":"hello world"}

# 设置 Stdout 输出

log := zerolog.New(os.Stdout).With().
    Str("foo", "bar").
    Logger()
stdlog.SetFlags(0)
stdlog.SetOutput(log)
stdlog.Print("hello world")
// Output: {"foo":"bar","message":"hello world"}

# Context 集成

Go context 在 Go 的代码中到处被使用,如果将 logger 嵌入到 context 中,Logger 就更方便传递到难以注入的地方。
Logger 实例可以使用 Logger.WithContext(ctx) 附加到 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。 
    // 如果 `Logger` 为 nil ,那么返回 `zerolog.DefaultContextLogger`
    // 如果 `DefaultContextLogger` 为 nil, 则返回 disabled logger
    logger := zerolog.Ctx(ctx)
    logger.Info().Msg("Hello")
}

第二种集成方式:

将当前 context.Context 传递到日志的事件中,并从 hooks 中检索它。
这对追踪存储在 go Context 中的信息 或者 logID 很有帮助。

type TracingHook struct{}
func (h TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
    ctx := e.Ctx()
    spanId := getSpanIdFromContext(ctx) // 根据跟踪框架来设定 logID
    e.Str("span-id", spanId)
}
func f() {
    // Setup the logger
    logger := zerolog.New(os.Stdout)
    logger = logger.Hook(TracingHook{})
    ctx := context.Background()
    // Use the Ctx function to make the context available to the hook
    logger.Info().Ctx(ctx).Msg("Hello")
}

# 多日志输出

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!")
}
// Output (Line 1: Console; Line 2: Stdout)
// 12:36PM INF Hello World!
// {"level":"info","time":"2019-11-07T12:36:38+03:00","message":"Hello World!"}

# 全局设置

某些设置可以更改并将应用于所有 logger:

  • log.Logger :设置此值来自定义全局 Logger(包级别方法使用的 Logger)。
  • zerolog.SetGlobalLevel :提高所有 Logger 的最低级别。使用 zerolog.Disabled 调用此命令可完全禁用日志记录(安静模式)。
  • zerolog.DisableSampling :如果参数为 true ,所有采样的 Logger 将停止采样并输出 100% 的日志事件。
  • zerolog.TimestampFieldName :设置自定义 Timestamp 字段名称。
  • zerolog.LevelFieldName :设置自定义级别字段名称。
  • zerolog.MessageFieldName :设置自定义消息字段名称。
  • zerolog.ErrorFieldName :设置自定义 Err 字段名称。
  • zerolog.TimeFieldFormat :设置自定义 Time 字段值格式。如果使用 zerolog.TimeFormatUnixzerolog.TimeFormatUnixMszerolog.TimeFormatUnixMicro 设置,时间将被格式化为 UNIX 时间戳。
  • zerolog.DurationFieldUnit :设置自定义时间单位。 Dur 添加的持续时间类型字段(默认: time.Millisecond )。
  • zerolog.DurationFieldInteger :设置为 true ,则 Dur 字段将格式化为整数而不是浮点数(默认值: false )。
  • zerolog.ErrorHandler :当 Zerolog 无法在其输出上写入事件时调用。如果未设置,则会在 stderr 上打印错误。该处理程序必须是线程安全且非阻塞的。

# 设置全局日志级别

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()
    // Default level for this example is info, unless debug flag is present
    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() {
        // Compute log output only if enabled.
        value := "bar"
        e.Str("foo", value).Msg("some debug message")
    }
}
$ ./logLevelExample
{"time":1516387492,"level":"info","message":"This message appears when log level set to Debug or Info"}
$ ./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"}

# 设定全局初始化字段名称

zerolog.TimestampFieldName = "t"
zerolog.LevelFieldName = "l"
zerolog.MessageFieldName = "m"
log.Info().Msg("hello world")
// Output: {"l":"info","t":1494567715,"m":"hello world"}

# 设定全局键值对日志

log.Logger = log.With().Str("service", "gin").Logger()
// Output: {"level":"info","service":"gin","time":"2023-09-14 10:08:41","message":"init log format"}

# 设定全局文件和行号

log.Logger = log.With().Caller().Logger()
log.Info().Msg("hello world")
// Output: {"level": "info", "message": "hello world", "caller": "/go/src/your_project/some_file:21"}

相当于 Lshortfile

zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
    short := file
    for i := len(file) - 1; i > 0; i-- {
        if file[i] == '/' {
            short = file[i+1:]
            break
        }
    }
    file = short
    return file + ":" + strconv.Itoa(line)
}
log.Logger = log.With().Caller().Logger()
log.Info().Msg("hello world")
// Output: {"level": "info", "message": "hello world", "caller": "some_file:21"}

# 错误日志

# Error log

package main
import (
	"errors"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)
func main() {
	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
	err := errors.New("seems we have an error here")
	log.Error().Err(err).Msg("")
}
// Output: {"level":"error","error":"seems we have an error here","time":1609085256}

# 使用 Stacktrace 记录错误

使用 github.com/pkg/errors ,可以用格式化的 Stacktrace 追踪错误信息

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("seems we have an error here")
}
func middle() error {
	err := inner()
	if err != nil {
		return err
	}
	return nil
}
func outer() error {
	err := middle()
	if err != nil {
		return err
	}
	return nil
}
// Output: {"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":"seems we have an error here","time":1609086683}

必须设置 Zerolog.ErrorStackMarshaler 才能使堆栈输出任何内容。

# Fatal log

package main
import (
    "errors"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)
func main() {
    err := errors.New("A repo man spends his life getting into tense situations")
    service := "myservice"
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    log.Fatal().
        Err(err).
        Str("service", service).
        Msgf("Cannot start %s", service)
}
// Output: {"time":1516133263,"level":"fatal","error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"}
//         exit status 1

# 注意事项

# 字段重复

zerolog 不会对重复字段进行删除。多次使用相同的字段会在最终 JSON 一起出现

一般情况下,程序会取最后一个字段的值,但是最好还是检查你的代码。

logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().
       Timestamp().
       Msg("dup")
// Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}

# 并发安全

调用 UpdateContext 时要小心。它不是并发安全的。使用 With 方法创建子 logger:

func handler(w http.ResponseWriter, r *http.Request) {
    // Create a child logger for concurrency safety
    logger := log.Logger.With().Logger()
    // Add context fields, for example User-Agent from HTTP headers
    logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
        ...
    })
}
更新于
-->