zerolog

什么是 Zerolog ?

zerolog 包提供了一个专门用于 JSON 输出的简单快速的Logger。

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

uber 的 zap 库开创了这种方法,zerolog 通过更简单的应用编程接口和更好的性能,将这一概念提升到了更高的层次。

使用 zerolog

安装

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

Contextual Logger

  1. func TestContextualLogger(t *testing.T) {
  2. log := zerolog.New(os.Stdout)
  3. log.Info().Str("content", "Hello world").Int("count", 3).Msg("TestContextualLogger")
  4. // 添加上下文 (文件名/行号/字符串)
  5. log = log.With().Caller().Str("foo", "bar").Logger()
  6. log.Info().Msg("Hello wrold")
  7. }

输出

  1. // {"level":"info","content":"Hello world","count":3,"message":"TestContextualLogger"}
  2. // {"level":"info","caller":"log_example_test.go:29","message":"Hello wrold"}

与 zap 相同的是,都定义了强类型字段,你可以在这里找到支持字段的完整列表。

与 zap 不同的是,zerolog 采用链式调用。

多级Logger

zerolog 提供了从 TracePanic 七个级别

  1. // 设置日志级别
  2. zerolog.SetGlobalLevel(zerolog.WarnLevel)
  3. log.Trace().Msg("Trace")
  4. log.Debug().Msg("Debug")
  5. log.Info().Msg("Info")
  6. log.Warn().Msg("Warn")
  7. log.Error().Msg("Error")
  8. log.Log().Msg("没有级别")

输出

  1. {"level":"warn","message":"Warn"}
  2. {"level":"error","message":"Error"}
  3. {"message":"没有级别"}

注意事项

1.zerolog 不会对重复的字段删除

  1. logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
  2. logger.Info().
  3. Timestamp().
  4. Msg("dup")

输出

  1. {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}

2.链式调用必须调用 MsgMsgfSend 才能输出日志,Send 相当于调用 Msg(“”)

3.一旦调用 MsgEvent 将会被处理 ( 放回池中或丢掉 ),不允许二次调用。

了解源码

本次zerolog的源码分析基于 zerolog 1.22.0 版本,源码分析较长,希望大家耐心看完。希望大家能有所收获。

看一下 Logger 结构体

Logger 的参数 w 类型是 LevelWriter 接口,用于向目标输出事件。zerolog.New 函数用来创建 Logger,看下方源码。

  1. // ============ log.go ===
  2. type Logger struct {
  3. w LevelWriter // 输出对象
  4. level Level // 日志级别
  5. sampler Sampler // 采样器
  6. context []byte // 存储上下文
  7. hooks []Hook
  8. stack bool
  9. }
  10. func New(w io.Writer) Logger {
  11. if w == nil {
  12. // ioutil.Discard 所有成功执行的 Write 操作都不会产生任何实际的效果
  13. w = ioutil.Discard
  14. }
  15. lw, ok := w.(LevelWriter)
  16. // 传入的不是 LevelWriter 类型,封装成此类型
  17. if !ok {
  18. lw = levelWriterAdapter{w}
  19. }
  20. // 默认输出日志级别 TraceLevel
  21. return Logger{w: lw, level: TraceLevel}
  22. }

debug 了解输出日志流程

每日一库之86:zerolog - 图1

如上图所示,在第三行打上断点。

下图表示该行代码执行流程。

每日一库之86:zerolog - 图2

开始 debug

  1. // ============ log.go ===
  2. // Info 开始记录一条 info 级别的消息
  3. // 你必须在返回的 *Event 上调用 Msg 才能发送事件
  4. func (l *Logger) Info() *Event {
  5. return l.newEvent(InfoLevel, nil)
  6. }
  7. func (l *Logger) newEvent(level Level, done func(string)) *Event {
  8. // 判断是否应该记录的级别
  9. enabled := l.should(level)
  10. if !enabled {
  11. return nil
  12. }
  13. // 创建记录日志的对象
  14. e := newEvent(l.w, level)
  15. // 设置 done 函数
  16. e.done = done
  17. // 设置 hook 函数
  18. e.ch = l.hooks
  19. // 记录日志级别
  20. if level != NoLevel && LevelFieldName != "" {
  21. e.Str(LevelFieldName, LevelFieldMarshalFunc(level))
  22. }
  23. // 记录上下文
  24. if l.context != nil && len(l.context) > 1 {
  25. e.buf = enc.AppendObjectData(e.buf, l.context)
  26. }
  27. // 堆栈跟踪
  28. if l.stack {
  29. e.Stack()
  30. }
  31. return e
  32. }

should 函数用于判断是否需要记录本次消息。

  1. // ============ log.go ===
  2. // should 如果应该被记录,则返回 true
  3. func (l *Logger) should(lvl Level) bool {
  4. if lvl < l.level || lvl < GlobalLevel() {
  5. return false
  6. }
  7. // 采样后面讲
  8. if l.sampler != nil && !samplingDisabled() {
  9. return l.sampler.Sample(lvl)
  10. }
  11. return true
  12. }

newEvent 函数使用 sync.Pool 获取Event对象,并将 Event 参数初始化:日志级别level和写入对象LevelWriter

  1. // ============ event.go ===
  2. // 表示一个日志事件
  3. type Event struct {
  4. buf []byte // 消息
  5. w LevelWriter // 待写入的目标接口
  6. level Level // 日志级别
  7. done func(msg string) // msg 函数结束事件
  8. stack bool // 错误堆栈跟踪
  9. ch []Hook // hook 函数组
  10. skipFrame int
  11. }
  12. func newEvent(w LevelWriter, level Level) *Event {
  13. e := eventPool.Get().(*Event)
  14. e.buf = e.buf[:0]
  15. e.ch = nil
  16. // 在开始添加左大括号 '{'
  17. e.buf = enc.AppendBeginMarker(e.buf)
  18. e.w = w
  19. e.level = level
  20. e.stack = false
  21. e.skipFrame = 0
  22. return e
  23. }

Str 函数是负责将键值对添加到 buf,字符串类型添加到 JSON 格式,涉及到特殊字符编码问题,如果是特殊字符,调用 appendStringComplex 函数解决。

  1. // ============ event.go ===
  2. func (e *Event) Str(key, val string) *Event {
  3. if e == nil {
  4. return e
  5. }
  6. e.buf = enc.AppendString(enc.AppendKey(e.buf, key), val)
  7. return e
  8. }
  9. // ============ internal/json/base.go ===
  10. type Encoder struct{}
  11. // 添加一个新 key
  12. func (e Encoder) AppendKey(dst []byte, key string) []byte {
  13. // 非第一个参数,加个逗号
  14. if dst[len(dst)-1] != '{' {
  15. dst = append(dst, ',')
  16. }
  17. return append(e.AppendString(dst, key), ':')
  18. }
  19. // === internal/json/string.go ===
  20. func (Encoder) AppendString(dst []byte, s string) []byte {
  21. // 双引号起
  22. dst = append(dst, '"')
  23. // 遍历字符
  24. for i := 0; i < len(s); i++ {
  25. // 检查字符是否需要编码
  26. if !noEscapeTable[s[i]] {
  27. dst = appendStringComplex(dst, s, i)
  28. return append(dst, '"')
  29. }
  30. }
  31. // 不需要编码的字符串,添加到 dst
  32. dst = append(dst, s...)
  33. // 双引号收
  34. return append(dst, '"')
  35. }

Int 函数将键值(int类型)对添加到 buf,内部调用 strconv.AppendInt 函数实现。

  1. // ============ event.go ===
  2. func (e *Event) Int(key string, i int) *Event {
  3. if e == nil {
  4. return e
  5. }
  6. e.buf = enc.AppendInt(enc.AppendKey(e.buf, key), i)
  7. return e
  8. }
  9. // === internal/json/types.go ===
  10. func (Encoder) AppendInt(dst []byte, val int) []byte {
  11. // 添加整数
  12. return strconv.AppendInt(dst, int64(val), 10)
  13. }

Msg 函数

  1. // === event.go ===
  2. // Msg 是对 msg 的封装调用,当指针接收器为 nil 返回
  3. func (e *Event) Msg(msg string) {
  4. if e == nil {
  5. return
  6. }
  7. e.msg(msg)
  8. }
  9. // msg
  10. func (e *Event) msg(msg string) {
  11. // 运行 hook
  12. for _, hook := range e.ch {
  13. hook.Run(e, e.level, msg)
  14. }
  15. // 记录消息
  16. if msg != "" {
  17. e.buf = enc.AppendString(enc.AppendKey(e.buf, MessageFieldName), msg)
  18. }
  19. // 判断不为 nil,则使用 defer 调用 done 函数
  20. if e.done != nil {
  21. defer e.done(msg)
  22. }
  23. // 写入日志
  24. if err := e.write(); err != nil {
  25. if ErrorHandler != nil {
  26. ErrorHandler(err)
  27. } else {
  28. fmt.Fprintf(os.Stderr, "zerolog: could not write event: %v\n", err)
  29. }
  30. }
  31. }
  32. // 写入日志
  33. func (e *Event) write() (err error) {
  34. if e == nil {
  35. return nil
  36. }
  37. if e.level != Disabled {
  38. // 大括号收尾
  39. e.buf = enc.AppendEndMarker(e.buf)
  40. // 换行
  41. e.buf = enc.AppendLineBreak(e.buf)
  42. // 向目标写入日志
  43. if e.w != nil {
  44. // 这里传递的日志级别,函数内并没有使用
  45. _, err = e.w.WriteLevel(e.level, e.buf)
  46. }
  47. }
  48. // 将对象放回池中
  49. putEvent(e)
  50. return
  51. }
  52. // === writer.go ===
  53. func (lw levelWriterAdapter) WriteLevel(l Level, p []byte) (n int, err error) {
  54. return lw.Write(p)
  55. }

以上 debug 让我们对日志记录流程有了大概的认识,接下来扩充一下相关知识。

从 zerolog 学习避免内存分配

每一条日志都会产生一个 Event对象 ,当多个 Goroutine 操作日志,导致创建的对象数目剧增,进而导致 GC 压力增大。形成 “并发大 - 占用内存大 - GC 缓慢 - 处理并发能力降低 - 并发更大”** 这样的恶性循环。在这个时候,需要有一个对象池,程序不再自己单独创建对象,而是从对象池中获取。

使用 sync.Pool 可以将暂时不用的对象缓存起来,下次需要的时候从池中取,不用再次经过内存分配。

下面代码中 putEvent 函数,当对象中记录消息的 buf 不超过 64KB 时,放回池中。这里有个链接,通过这个 issue 23199了解到使用动态增长的 buffer 会导致大量内存被固定,在活锁的情况下永远不会释放。

  1. var eventPool = &sync.Pool{
  2. New: func() interface{} {
  3. return &Event{
  4. buf: make([]byte, 0, 500),
  5. }
  6. },
  7. }
  8. func putEvent(e *Event) {
  9. // 选择占用较小内存的 buf,将对象放回池中
  10. // See https://golang.org/issue/23199
  11. const maxSize = 1 << 16 // 64KiB
  12. if cap(e.buf) > maxSize {
  13. return
  14. }
  15. eventPool.Put(e)
  16. }

学习日志级别

下面代码中,包含了日志级别类型的定义,日志级别对应的字符串值,获取字符串值的方法以及解析字符串为日志级别类型的方法。

  1. // ============= log.go ===
  2. // 日志级别类型
  3. type Level int8
  4. // 定义所有日志级别
  5. const (
  6. DebugLevel Level = iota
  7. InfoLevel
  8. WarnLevel
  9. ErrorLevel
  10. FatalLevel
  11. PanicLevel
  12. NoLevel
  13. Disabled
  14. TraceLevel Level = -1
  15. )
  16. // 返回当前级别的 value
  17. func (l Level) String() string {
  18. switch l {
  19. case TraceLevel:
  20. return LevelTraceValue
  21. case DebugLevel:
  22. return LevelDebugValue
  23. case InfoLevel:
  24. return LevelInfoValue
  25. case WarnLevel:
  26. return LevelWarnValue
  27. case ErrorLevel:
  28. return LevelErrorValue
  29. case FatalLevel:
  30. return LevelFatalValue
  31. case PanicLevel:
  32. return LevelPanicValue
  33. case Disabled:
  34. return "disabled"
  35. case NoLevel:
  36. return ""
  37. }
  38. return ""
  39. }
  40. // ParseLevel 将级别字符串解析成 zerolog level value
  41. // 当字符串不匹配任何已知级别,返回错误
  42. func ParseLevel(levelStr string) (Level, error) {
  43. switch levelStr {
  44. case LevelFieldMarshalFunc(TraceLevel):
  45. return TraceLevel, nil
  46. case LevelFieldMarshalFunc(DebugLevel):
  47. return DebugLevel, nil
  48. case LevelFieldMarshalFunc(InfoLevel):
  49. return InfoLevel, nil
  50. case LevelFieldMarshalFunc(WarnLevel):
  51. return WarnLevel, nil
  52. case LevelFieldMarshalFunc(ErrorLevel):
  53. return ErrorLevel, nil
  54. case LevelFieldMarshalFunc(FatalLevel):
  55. return FatalLevel, nil
  56. case LevelFieldMarshalFunc(PanicLevel):
  57. return PanicLevel, nil
  58. case LevelFieldMarshalFunc(Disabled):
  59. return Disabled, nil
  60. case LevelFieldMarshalFunc(NoLevel):
  61. return NoLevel, nil
  62. }
  63. return NoLevel, fmt.Errorf("Unknown Level String: '%s', defaulting to NoLevel", levelStr)
  64. }
  65. // ============= globals.go ===
  66. var (
  67. // ......
  68. // 级别字段的 key 名称
  69. LevelFieldName = "level"
  70. // 各个级别的 value
  71. LevelTraceValue = "trace"
  72. LevelDebugValue = "debug"
  73. LevelInfoValue = "info"
  74. LevelWarnValue = "warn"
  75. LevelErrorValue = "error"
  76. LevelFatalValue = "fatal"
  77. LevelPanicValue = "panic"
  78. // 返回形参级别的 value
  79. LevelFieldMarshalFunc = func(l Level) string {
  80. return l.String()
  81. }
  82. // ......
  83. )

全局日志级别参数

这里使用 atomic 来保证原子操作,要么都执行,要么都不执行,外界不会看到只执行到一半的状态,原子操作由底层硬件支持,通常比锁更有效率。

atomic.StoreInt32 用于存储 int32 类型的值。

atomic.LoadInt32 用于读取 int32 类型的值。

在源码中,做级别判断时,多处调用 GlobalLevel 以保证并发安全。

  1. // ============= globals.go ===
  2. var (
  3. gLevel = new(int32)
  4. // ......
  5. )
  6. // SetGlobalLevel 设置全局日志级别
  7. // 要全局禁用日志,入参为 Disabled
  8. func SetGlobalLevel(l Level) {
  9. atomic.StoreInt32(gLevel, int32(l))
  10. }
  11. // 返回当前全局日志级别
  12. func GlobalLevel() Level {
  13. return Level(atomic.LoadInt32(gLevel))
  14. }

学习如何实现 Hook

首先定义 Hook 接口,内部有一个 Run 函数,入参包含 Event,日志级别level和消息 ( Msg 函数的参数 )。

然后定义了 LevelHook 结构体,用于为每个级别设置 Hook 。

  1. // ============= hook.go ===
  2. // hook 接口
  3. type Hook interface {
  4. Run(e *Event, level Level, message string)
  5. }
  6. // HookFunc 函数适配器
  7. type HookFunc func(e *Event, level Level, message string)
  8. // Run 实现 Hook 接口.
  9. func (h HookFunc) Run(e *Event, level Level, message string) {
  10. h(e, level, message)
  11. }
  12. // 为每个级别应用不同的 hook
  13. type LevelHook struct {
  14. NoLevelHook, TraceHook, DebugHook, InfoHook, WarnHook, ErrorHook, FatalHook, PanicHook Hook
  15. }
  16. // Run 实现 Hook 接口
  17. func (h LevelHook) Run(e *Event, level Level, message string) {
  18. switch level {
  19. case TraceLevel:
  20. if h.TraceHook != nil {
  21. h.TraceHook.Run(e, level, message)
  22. }
  23. case DebugLevel:
  24. if h.DebugHook != nil {
  25. h.DebugHook.Run(e, level, message)
  26. }
  27. case InfoLevel:
  28. if h.InfoHook != nil {
  29. h.InfoHook.Run(e, level, message)
  30. }
  31. case WarnLevel:
  32. if h.WarnHook != nil {
  33. h.WarnHook.Run(e, level, message)
  34. }
  35. case ErrorLevel:
  36. if h.ErrorHook != nil {
  37. h.ErrorHook.Run(e, level, message)
  38. }
  39. case FatalLevel:
  40. if h.FatalHook != nil {
  41. h.FatalHook.Run(e, level, message)
  42. }
  43. case PanicLevel:
  44. if h.PanicHook != nil {
  45. h.PanicHook.Run(e, level, message)
  46. }
  47. case NoLevel:
  48. if h.NoLevelHook != nil {
  49. h.NoLevelHook.Run(e, level, message)
  50. }
  51. }
  52. }
  53. // NewLevelHook 创建一个 LevelHook
  54. func NewLevelHook() LevelHook {
  55. return LevelHook{}
  56. }

在源码中是如何使用的?

定义 PrintMsgHook 结构体并实现 Hook 接口,作为参数传递给 log.Hook 函数,Logger 内部的 hooks 参数用来保存对象。

  1. // 使用案例
  2. type PrintMsgHook struct{}
  3. // 实现 Hook 接口,用来向控制台输出 msg
  4. func (p PrintMsgHook) Run(e *zerolog.Event, l zerolog.Level, msg string) {
  5. fmt.Println(msg)
  6. }
  7. func TestContextualLogger(t *testing.T) {
  8. log := zerolog.New(os.Stdout)
  9. log = log.Hook(PrintMsgHook{})
  10. log.Info().Msg("TestContextualLogger")
  11. }

添加 hook 源码如下

  1. // ============ log.go ===
  2. // Hook 返回一个带有 hook 的 Logger
  3. func (l Logger) Hook(h Hook) Logger {
  4. l.hooks = append(l.hooks, h)
  5. return l
  6. }

输出日志必须调用 msg 函数,hook 将在此函数的开头执行。

  1. // ============ event.go ===
  2. // msg 函数用来运行 hook
  3. func (e *Event) msg(msg string) {
  4. for _, hook := range e.ch {
  5. hook.Run(e, e.level, msg)
  6. }
  7. // .......
  8. // 写入日志,此函数上面已经介绍过,此处省略
  9. // .......
  10. }

学习如何得到调用者函数名

在看 zerolog 源码之前,需要知道一些关于 runtime.Caller 函数的前置知识,

  • runtime.Caller 可以获取相关调用 goroutine 堆栈上的函数调用的文件和行号信息。
  • 参数skip 是堆栈帧的数量,当 skip=0 时,输出当前函数信息; 当 skip=1 时,输出调用栈上一帧,即调用函数者的信息。
  • 返回值为 程序计数器,文件位置,行号,是否能恢复信息
  1. // ============ go@1.16.5 runtime/extern.go ===
  2. func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
  3. rpc := make([]uintptr, 1)
  4. n := callers(skip+1, rpc[:])
  5. if n < 1 {
  6. return
  7. }
  8. frame, _ := CallersFrames(rpc).Next()
  9. return frame.PC, frame.File, frame.Line, frame.PC != 0
  10. }

再看 zerolog 源码,定义 callerHook 结构体并实现了 Hook 接口,实现函数中调用了参数 Event 提供的 caller** 函数。

其中入参为预定义参数 CallerSkipFrameCountcontextCallerSkipFrameCount ,值都为 2。

每日一库之86:zerolog - 图3

  1. // ============ context.go ===
  2. type callerHook struct {
  3. callerSkipFrameCount int
  4. }
  5. func newCallerHook(skipFrameCount int) callerHook {
  6. return callerHook{callerSkipFrameCount: skipFrameCount}
  7. }
  8. func (ch callerHook) Run(e *Event, level Level, msg string) {
  9. switch ch.callerSkipFrameCount {
  10. // useGlobalSkipFrameCount 是 int32 类型最小值
  11. case useGlobalSkipFrameCount:
  12. // CallerSkipFrameCount 预定义全局变量,值为 2
  13. // contextCallerSkipFrameCount 预定义变量,值为 2
  14. e.caller(CallerSkipFrameCount + contextCallerSkipFrameCount)
  15. default:
  16. e.caller(ch.callerSkipFrameCount + contextCallerSkipFrameCount)
  17. }
  18. }
  19. // useGlobalSkipFrameCount 值:-2147483648
  20. const useGlobalSkipFrameCount = math.MinInt32
  21. // 创建默认 callerHook
  22. var ch = newCallerHook(useGlobalSkipFrameCount)
  23. // Caller 为 Logger 添加 hook ,该 hook 用于记录函数调用者的 file:line
  24. func (c Context) Caller() Context {
  25. c.l = c.l.Hook(ch)
  26. return c
  27. }
  1. // ============ event.go ===
  2. func (e *Event) caller(skip int) *Event {
  3. if e == nil {
  4. return e
  5. }
  6. _, file, line, ok := runtime.Caller(skip + e.skipFrame)
  7. if !ok {
  8. return e
  9. }
  10. // CallerFieldName是默认的 key 名
  11. // CallerMarshalFunc 函数用于拼接 file:line
  12. e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), CallerMarshalFunc(file, line))
  13. return e
  14. }

从日志采样中学习 atomic

这个使用案例中,TestSample 每秒允许 记录5 条消息,超过则每 20 条仅记录一条

  1. func TestSample(t *testing.T) {
  2. sampled := log.Sample(&zerolog.BurstSampler{
  3. Burst: 5,
  4. Period: 1 * time.Second,
  5. NextSampler: &zerolog.BasicSampler{N: 20},
  6. })
  7. for i := 0; i <= 50; i++ {
  8. sampled.Info().Msgf("logged messages : %2d ", i)
  9. }
  10. }

输出结果本来应该输出 50 条日志,使用了采样,在一秒内输出最大 5 条日志,当大于 5 条后,每 20 条日志输出一次。

每日一库之86:zerolog - 图4

采样的流程示意图如下

每日一库之86:zerolog - 图5

下方是定义采样接口及实现函数的源码。

inc 函数中,使用 atomic 包将竞争的接收器对象的参数变成局部变量,是学习 atomic 很好的实例。函数说明都写在注释里。

  1. // =========== sampler.go ===
  2. // 采样器接口
  3. type Sampler interface {
  4. // 如果事件是样本的一部分返回 true
  5. Sample(lvl Level) bool
  6. }
  7. // BasicSampler 基本采样器
  8. // 每 N 个事件发送一次,不考虑日志级别
  9. type BasicSampler struct {
  10. N
  11. counter uint32
  12. }
  13. // 实现采样器接口
  14. func (s *BasicSampler) Sample(lvl Level) bool {
  15. n := s.N
  16. if n == 1 {
  17. return true
  18. }
  19. c := atomic.AddUint32(&s.counter, 1)
  20. return c%n == 1
  21. }
  22. type BurstSampler struct {
  23. // 调用 NextSampler 之前每个时间段(Period)调用的最大事件数量
  24. Burst uint32
  25. // 如果为 0,则始终调用 NextSampler
  26. Period time.Duration
  27. // 采样器
  28. NextSampler Sampler
  29. // 用于计数在一定时间内(Period)的调用数量
  30. counter uint32
  31. // 时间段的结束时间(纳秒),即 当前时间+Period
  32. resetAt int64
  33. }
  34. // 实现 Sampler 接口
  35. func (s *BurstSampler) Sample(lvl Level) bool {
  36. // 当设置了 Burst 和 Period,大于零时限制 一定时间内的最大事件数量
  37. if s.Burst > 0 && s.Period > 0 {
  38. if s.inc() <= s.Burst {
  39. return true
  40. }
  41. }
  42. // 没有采样器,结束
  43. if s.NextSampler == nil {
  44. return false
  45. }
  46. // 调用采样器
  47. return s.NextSampler.Sample(lvl)
  48. }
  49. func (s *BurstSampler) inc() uint32 {
  50. // 当前时间 (纳秒)
  51. now := time.Now().UnixNano()
  52. // 重置时间 (纳秒)
  53. resetAt := atomic.LoadInt64(&s.resetAt)
  54. var c uint32
  55. // 当前时间 > 重置时间
  56. if now > resetAt {
  57. c = 1
  58. // 重置 s.counter 为 1
  59. atomic.StoreUint32(&s.counter, c)
  60. // 计算下一次的重置时间
  61. newResetAt := now + s.Period.Nanoseconds()
  62. // 比较函数开头获取的重置时间与存储的时间是否相等
  63. // 相等时,将下一次的重置时间存储到 s.resetAt,并返回 true
  64. reset := atomic.CompareAndSwapInt64(&s.resetAt, resetAt, newResetAt)
  65. if !reset {
  66. // 在上面比较赋值那一步没有抢到的 goroutine 计数器+1
  67. c = atomic.AddUint32(&s.counter, 1)
  68. }
  69. } else {
  70. c = atomic.AddUint32(&s.counter, 1)
  71. }
  72. return c
  73. }

在代码中如何调用的呢?

Info 函数及其他级别函数都会调用 newEvent,在该函数的开头, should 函数用来判断是否需要记录的日志级别和采样。

  1. // ============ log.go ===
  2. // should 如果应该被记录,则返回 true
  3. func (l *Logger) should(lvl Level) bool {
  4. if lvl < l.level || lvl < GlobalLevel() {
  5. return false
  6. }
  7. // 如果使用了采样,则调用采样函数,判断本次事件是否记录
  8. if l.sampler != nil && !samplingDisabled() {
  9. return l.sampler.Sample(lvl)
  10. }
  11. return true
  12. }

Doc

关于更多zerolog的使用可以参考 https://pkg.go.dev/github.com/rs/zerolog

比较

说明 : 以下资料来源于 zerolog 官方。从性能分析上zerolog比zap和其他logger库更胜一筹,关于zerolog和zap的使用,gopher可根据实际业务场景具体考量。

记录 10 个 KV 字段的消息 :

Library Time Bytes Allocated Objects Allocated
zerolog 767 ns/op 552 B/op 6 allocs/op
⚡ zap 848 ns/op 704 B/op 2 allocs/op
⚡ 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 个 KV 字段的 logger 记录一条消息 :

Library Time Bytes Allocated Objects Allocated
zerolog 52 ns/op 0 B/op 0 allocs/op
⚡ zap 283 ns/op 0 B/op 0 allocs/op
⚡ 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 风格的模板 :

Library Time Bytes Allocated Objects Allocated
zerolog 50 ns/op 0 B/op 0 allocs/op
⚡ zap 236 ns/op 0 B/op 0 allocs/op
standard library 453 ns/op 80 B/op 2 allocs/op
⚡ 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

相似的库

logrus 功能强大

zap 非常快速,结构化,分级

参考资料

zerolog 官方文档