简介

每个编程语言都有很多日志库,因为记录日志在每个项目中都是必须的。前面我们介绍了标准日志库log、好用的logrus和上一篇文章中介绍的由 uber 开源的高性能日志库zapzerolog相比zap更进了一步,它的 API 设计非常注重开发体验和性能。zerolog只专注于记录 JSON 格式的日志,号称 0 内存分配!

快速使用

先安装:

1
$ go get github.com/rs/zerolog/log

后使用:

1
2
3
4
5
6
7
package main

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

func main() {
  log.Print("hello world")
}

常规使用与标准库log非常相似,只不过输出的是 JSON 格式的日志:

1
{"level":"debug","time":"2020-04-25T13:43:08+08:00","message":"hello world"}

字段

我们可以在日志中添加额外的字段信息,有助于调试和问题追踪。与zap一样,zerolog也区分字段类型,不同的是zerolog采用链式调用的方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
  log.Debug().
    Str("Scale", "833 cents").
    Float64("Interval", 833.09).
    Msg("Fibonacci is everywhere")

  log.Debug().
    Str("Name", "Tom").
    Send()
}

调用Msg()Send()之后,日志会被输出:

1
2
{"level":"debug","Scale":"833 cents","Interval":833.09,"time":"2020-04-25T13:55:44+08:00","message":"Fibonacci is everywhere"}
{"level":"debug","Name":"Tom","time":"2020-04-25T13:55:44+08:00"}

嵌套

记录的字段可以任意嵌套,这通过Dict()来实现:

1
2
3
4
5
6
7
func main() {
  log.Info().
    Dict("dict", zerolog.Dict().
      Str("bar", "baz").
      Int("n", 1),
    ).Msg("hello world")
}

输出中dict字段为嵌套结构:

1
{"level":"info","dict":{"bar":"baz","n":1},"time":"2020-04-25T14:34:51+08:00","message":"hello world"}

全局Logger

上面我们使用log.Debug()log.Info()调用的是全局的Logger。全局的Logger使用比较简单,不需要额外创建。

设置日志级别

每个日志库都有日志级别的概念,而且划分基本上都差不多。zerologpanic/fatal/error/warn/info/debug/trace这几种级别。我们可以调用SetGlobalLevel()设置全局Logger的日志级别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main() {
  debug := flag.Bool("debug", false, "sets log level to debug")
  flag.Parse()

  if *debug {
    zerolog.SetGlobalLevel(zerolog.DebugLevel)
  } else {
    zerolog.SetGlobalLevel(zerolog.InfoLevel)
  }

  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() {
    e.Str("foo", "bar").Msg("some debug message")
  }
}

在上面代码中,我们根据传入的命令行选项设置日志级别是Debug还是Info。如果日志级别为InfoDebug的日志是不会输出的。也可以调用Enabled()方法来判断日志是否需要输出,需要时再调用相应方法输出,节省了添加字段和日志信息的开销:

1
2
3
if e := log.Debug(); e.Enabled() {
  e.Str("foo", "bar").Msg("some debug message")
}

先不加命令行参数运行,默认为Info级别,Debug日志不会输出:

1
2
$ go run main.go
{"level":"info","time":"2020-04-25T14:13:34+08:00","message":"This message appears when log level set to debug or info"}

加上-debug选项,DebugInfo日志都输出了:

1
2
3
4
5
$ go run main.go -debug
{"level":"debug","time":"2020-04-25T14:18:19+08:00","message":"This message appears only when log level set to debug"}
{"level":"info","time":"2020-04-25T14:18:19+08:00","message":"This message appears when log level set to debug or info"}
{"level":"debug","foo":"bar","time":"2020-04-25T14:18:19+08:00","message":"some debug 
message"}

不输出级别和信息

有时候我们不想输出日志级别(即level字段),这时可以使用log.Log()方法。有时,我们没有日志信息可输出,这时传一个空字符串给Msg()方法:

1
2
3
4
5
func main() {
  log.Log().
    Str("foo", "bar").
    Msg("")
}

运行:

1
{"foo":"bar","time":"2020-04-25T14:19:48+08:00"}

创建Logger

上面我们使用的都是全局的Logger,这种方式有一个明显的缺点:如果在某个地方修改了设置,将影响全局的日志记录。为了消除这种影响,我们需要创建新的Logger

1
2
3
4
func main() {
  logger := zerolog.New(os.Stderr)
  logger.Info().Str("foo", "bar").Msg("hello world")
}

调用zerlog.New()传入一个io.Writer作为日志写入器即可。

Logger

基于当前的Logger可以创建一个子Logger,子Logger可以在父Logger上附加一些额外的字段。调用logger.With()创建一个上下文,然后为它添加字段,最后调用Logger()返回一个新的Logger

1
2
3
4
5
6
7
func main() {
  logger := zerolog.New(os.Stderr)
  sublogger := logger.With().
    Str("foo", "bar").
    Logger()
  sublogger.Info().Msg("hello world")
}

sublogger会额外输出"foo": "bar"这个字段。

设置

zerolog提供了多种选项定制输入日志的行为。

美化输出

zerolog提供了一个ConsoleWriter可输出便于我们阅读的,带颜色的日志。调用zerolog.Output()来启用ConsoleWriter

1
2
3
4
func main() {
  logger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
  logger.Info().Str("foo", "bar").Msg("hello world")
}

输出:

我们还能进一步对ConsoleWriter进行配置,定制输出的级别、信息、字段名、字段值的格式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func main() {
  output := zerolog.ConsoleWriter{Out: os.Stderr, 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))
  }

  logger := log.Output(output).With().Timestamp().Logger()
  logger.Info().Str("foo", "bar").Msg("hello world")
}

实际上就是对级别、信息、字段名和字段值设置钩子,输出前经过钩子函数转换一次:

ConsoleWriter的性能不够理想,建议只在开发环境中使用!

设置自动添加的字段名

输出的日志中级别默认的字段名为level,信息默认为message,时间默认为time。可以通过zerologLevelFieldName/MessageFieldName/TimestampFieldName来设置:

1
2
3
4
5
6
7
8
func main() {
  zerolog.TimestampFieldName = "t"
  zerolog.LevelFieldName = "l"
  zerolog.MessageFieldName = "m"

  logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
  logger.Info().Msg("hello world")
}

输出:

1
{"l":"info","t":"2020-04-25T14:53:08+08:00","m":"hello world"}

注意,这个设置是全局的!!!

输出文件名和行号

有时我们需要输出文件名和行号,以便能很快定位代码位置,方便找出问题。这可以通过在创建子Logger时带入Caller()选项完成:

1
2
3
4
func main() {
  logger := zerolog.New(os.Stderr).With().Caller().Logger()
  logger.Info().Msg("hello world")
}

输出:

1
{"level":"info","caller":"d:/code/golang/src/github.com/darjun/go-daily-lib/zerolog/setting/file-line/main.go:11","message":"hello world"}

日志采样

有时候日志太多了反而对我们排查问题造成干扰,zerolog支持日志采样的功能,可以每隔多少条日志输出一次,其他日志丢弃:

1
2
3
4
5
6
7
func main() {
  sampled := log.Sample(&zerolog.BasicSampler{N: 10})

  for i := 0; i < 20; i++ {
    sampled.Info().Msg("will be logged every 10 message")
  }
}

结果只输出两条:

1
2
{"level":"info","time":"2020-04-25T15:01:02+08:00","message":"will be logged every 10 message"}
{"level":"info","time":"2020-04-25T15:01:02+08:00","message":"will be logged every 10 message"}

还有更高级的设置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
  sampled := log.Sample(&zerolog.LevelSampler{
    DebugSampler: &zerolog.BurstSampler{
      Burst:       5,
      Period:      time.Second,
      NextSampler: &zerolog.BasicSampler{N: 100},
    },
  })

  sampled.Debug().Msg("hello world")
}

上面代码只采样Debug日志,在 1s 内最多输出 5 条日志,超过 5条 时,每隔 100 条输出一条。

钩子

zerolog支持钩子,我们可以针对不同的日志级别添加一些额外的字段或进行其他的操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type AddFieldHook struct {
}

func (AddFieldHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
  if level == zerolog.DebugLevel {
    e.Str("name", "dj")
  }
}

func main() {
  hooked := log.Hook(AddFieldHook{})
  hooked.Debug().Msg("")
}

如果是Debug级别,额外输出"name":"dj"字段:

1
{"level":"debug","time":"2020-04-25T15:09:04+08:00","name":"dj"}

性能

关于性能,GitHub 上有一份详细的性能测试,与logrus/zap等日志库的比较。感兴趣可以去看看:https://github.com/rs/zerolog#benchmarkszerolog的性能比zap还要优秀!

总结

正是因为有很多人不满足于现状,才带来了技术的进步和丰富多彩的开源世界!

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. zerolog GitHub:https://github.com/rs/zerolog
  2. Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib

我的博客:https://darjun.github.io

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~