Go 每日一库之 zap

栏目: IT技术 · 发布时间: 4年前

内容简介:在很早之前的文章中,我们介绍过 Go 标准日志库先安装:后使用:

简介

在很早之前的文章中,我们介绍过 Go 标准日志库 log 和结构化的日志库 logrus 。在热点函数中记录日志对日志库的执行性能有较高的要求,不能影响正常逻辑的执行时间。 uber 开源的日志库 zap ,对性能和内存分配做了极致的优化。

快速使用

先安装:

$ go get go.uber.org/zap

后使用:

package main

import (
  "time"

  "go.uber.org/zap"
)

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  url := "http://example.org/api"
  logger.Info("failed to fetch URL",
    zap.String("url", url),
    zap.Int("attempt", 3),
    zap.Duration("backoff", time.Second),
  )

  sugar := logger.Sugar()
  sugar.Infow("failed to fetch URL",
    "url", url,
    "attempt", 3,
    "backoff", time.Second,
  )
  sugar.Infof("Failed to fetch URL: %s", url)
}

zap 库的使用与其他的日志库非常相似。先创建一个 logger ,然后调用各个级别的方法记录日志( Debug/Info/Error/Warn )。 zap 提供了几个快速创建 logger 的方法, zap.NewExample()zap.NewDevelopment()zap.NewProduction() ,还有高度定制化的创建方法 zap.New() 。创建前 3 个 logger 时, zap 会使用一些预定义的设置,它们的使用场景也有所不同。 Example 适合用在测试代码中, Development 在开发环境中使用, Production 用在生成环境。

zap 底层 API 可以设置缓存,所以一般使用 defer logger.Sync() 将缓存同步到文件中。

由于 fmt.Printf 之类的方法大量使用 interface{} 和反射,会有不少性能损失,并且增加了内存分配的频次。 zap 为了提高性能、减少内存分配次数,没有使用反射,而且默认的 Logger 只支持强类型的、结构化的日志。必须使用 zap 提供的方法记录字段。 zap 为 Go 语言中所有的基本类型和其他常见类型都提供了方法。这些方法的名称也比较好记忆, zap.TypeTypebool/int/uint/float64/complex64/time.Time/time.Duration/error 等)就表示该类型的字段, zap.Typepp 结尾表示该类型指针的字段, zap.Typess 结尾表示该类型切片的字段。如:

  • zap.Bool(key string, val bool) Fieldbool 字段
  • zap.Boolp(key string, val *bool) Fieldbool 指针字段;
  • zap.Bools(key string, val []bool) Fieldbool 切片字段。

当然也有一些特殊类型的字段:

zap.Any(key string, value interface{}) Field
zap.Binary(key string, val []byte) Field

当然,每个字段都用方法包一层用起来比较繁琐。 zap 也提供了便捷的方法 SugarLogger ,可以使用 printf 格式符的方式。调用 logger.Sugar() 即可创建 SugaredLoggerSugaredLogger 的使用比 Logger 简单,只是性能比 Logger 低 50% 左右,可以用在非热点函数中。调用 SugarLoggerf 结尾的方法与 fmt.Printf 没什么区别,如例子中的 Infof 。同时 SugarLogger 还支持以 w 结尾的方法,这种方式不需要先创建字段对象,直接将字段名和值依次放在参数中即可,如例子中的 Infow

默认情况下, Example 输出的日志为 JSON 格式:

{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"Failed to fetch URL: http://example.org/api"}

记录层级关系

前面我们记录的日志都是一层结构,没有嵌套的层级。我们可以使用 zap.Namespace(key string) Field 构建一个 命名空间 ,后续的 Field 都记录在此命名空间中:

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  logger.Info("tracked some metrics",
    zap.Namespace("metrics"),
    zap.Int("counter", 1),
  )

  logger2 := logger.With(
    zap.Namespace("metrics"),
    zap.Int("counter", 1),
  )
  logger2.Info("tracked some metrics")
}

输出:

{"level":"info","msg":"tracked some metrics","metrics":{"counter":1}}
{"level":"info","msg":"tracked some metrices","metrics":{"counter":1}}

上面我们演示了两种 Namespace 的用法,一种是直接作为字段传入 Debug/Info 等方法,一种是调用 With() 创建一个新的 Logger ,新的 Logger 记录日志时总是带上预设的字段。 With() 方法实际上是创建了一个新的 Logger

// src/go.uber.org/zap/logger.go
func (log *Logger) With(fields ...Field) *Logger {
  if len(fields) == 0 {
    return log
  }
  l := log.clone()
  l.core = l.core.With(fields)
  return l
}

定制 Logger

调用 NexExample()/NewDevelopment()/NewProduction() 这 3 个方法, zap 使用默认的配置。我们也可以手动调整,配置结构如下:

// src/go.uber.org/zap/config.go
type Config struct {
  Level AtomicLevel `json:"level" yaml:"level"`
  Encoding string `json:"encoding" yaml:"encoding"`
  EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
  OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
  ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
  InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}
  • Level :日志级别;
  • Encoding :输出的日志格式,默认为 JSON;
  • OutputPaths :可以配置多个输出路径,路径可以是文件路径和 stdout (标准输出);
  • ErrorOutputPaths :错误输出路径,也可以是多个;
  • InitialFields :每条日志中都会输出这些值。

其中 EncoderConfig 为编码配置:

// src/go.uber.org/zap/zapcore/encoder.go
type EncoderConfig struct {
  MessageKey    string `json:"messageKey" yaml:"messageKey"`
  LevelKey      string `json:"levelKey" yaml:"levelKey"`
  TimeKey       string `json:"timeKey" yaml:"timeKey"`
  NameKey       string `json:"nameKey" yaml:"nameKey"`
  CallerKey     string `json:"callerKey" yaml:"callerKey"`
  StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
  LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
  EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
  EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
  EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
  EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
  EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
}
  • MessageKey :日志中信息的键名,默认为 msg
  • LevelKey :日志中级别的键名,默认为 level
  • EncodeLevel :日志中级别的格式,默认为小写,如 debug/info

调用 zap.ConfigBuild() 方法即可使用该配置对象创建一个 Logger

func main() {
  rawJSON := []byte(`{
    "level":"debug",
    "encoding":"json",
    "outputPaths": ["stdout", "server.log"],
    "errorOutputPaths": ["stderr"],
    "initialFields":{"name":"dj"},
    "encoderConfig": {
      "messageKey": "message",
      "levelKey": "level",
      "levelEncoder": "lowercase"
    }
  }`)

  var cfg zap.Config
  if err := json.Unmarshal(rawJSON, &cfg); err != nil {
    panic(err)
  }
  logger, err := cfg.Build()
  if err != nil {
    panic(err)
  }
  defer logger.Sync()

  logger.Info("server start work successfully!")
}

上面创建一个输出到标准输出 stdout 和文件 server.logLogger 。观察输出:

{"level":"info","message":"server start work successfully!","name":"dj"}

使用 NewDevelopment() 创建的 Logger 使用的是如下的配置:

// src/go.uber.org/zap/config.go
func NewDevelopmentConfig() Config {
  return Config{
    Level:            NewAtomicLevelAt(DebugLevel),
    Development:      true,
    Encoding:         "console",
    EncoderConfig:    NewDevelopmentEncoderConfig(),
    OutputPaths:      []string{"stderr"},
    ErrorOutputPaths: []string{"stderr"},
  }
}

func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
  return zapcore.EncoderConfig{
    // Keys can be anything except the empty string.
    TimeKey:        "T",
    LevelKey:       "L",
    NameKey:        "N",
    CallerKey:      "C",
    MessageKey:     "M",
    StacktraceKey:  "S",
    LineEnding:     zapcore.DefaultLineEnding,
    EncodeLevel:    zapcore.CapitalLevelEncoder,
    EncodeTime:     zapcore.ISO8601TimeEncoder,
    EncodeDuration: zapcore.StringDurationEncoder,
    EncodeCaller:   zapcore.ShortCallerEncoder,
  }
}

NewProduction() 的配置可自行查看。

选项

NewExample()/NewDevelopment()/NewProduction() 这 3 个函数可以传入若干类型为 zap.Option 的选项,从而定制 Logger 的行为。又一次见到了 选项模式 !!

zap 提供了丰富的选项供我们选择。

输出文件名和行号

调用 zap.AddCaller() 返回的选项设置输出文件名和行号。但是有一个前提,必须设置配置对象 Config 中的 CallerKey 字段。也因此 NewExample() 不能输出这个信息(它的 Config 没有设置 CallerKey )。

func main() {
  logger, _ := zap.NewProduction(zap.AddCaller())
  defer logger.Sync()

  logger.Info("hello world")
}

输出:

{"level":"info","ts":1587740198.9508286,"caller":"caller/main.go:9","msg":"hello world"}

Info() 方法在 main.go 的第 9 行被调用。 AddCaller()zap.WithCaller(true) 等价。

有时我们稍微封装了一下记录日志的方法,但是我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用 zap.AddCallerSkip(skip int) 向上跳 1 层:

func Output(msg string, fields ...zap.Field) {
  zap.L().Info(msg, fields...)
}

func main() {
  logger, _ := zap.NewProduction(zap.AddCaller(), zap.AddCallerSkip(1))
  defer logger.Sync()

  zap.ReplaceGlobals(logger)

  Output("hello world")
}

输出:

{"level":"info","ts":1587740501.5592482,"caller":"skip/main.go:15","msg":"hello world"}

输出在 main 函数中调用 Output() 的位置。如果不指定 zap.AddCallerSkip(1) ,将输出 "caller":"skip/main.go:6" ,这是在 Output() 函数中调用 zap.Info() 的位置。因为这个 Output() 函数可能在很多地方被调用,所以这个位置参考意义并不大。试试看!

输出调用堆栈

有时候在某个函数处理中遇到了异常情况,因为这个函数可能在很多地方被调用。如果我们能输出此次调用的堆栈,那么分析起来就会很方便。我们可以使用 zap.AddStackTrace(lvl zapcore.LevelEnabler) 达成这个目的。该函数指定 lvl 和之上的级别都需要输出调用堆栈:

func f1() {
  f2("hello world")
}

func f2(msg string, fields ...zap.Field) {
  zap.L().Warn(msg, fields...)
}

func main() {
  logger, _ := zap.NewProduction(zap.AddStacktrace(zapcore.WarnLevel))
  defer logger.Sync()

  zap.ReplaceGlobals(logger)

  f1()
}

zapcore.WarnLevel 传入 AddStacktrace() ,之后 Warn()/Error() 等级别的日志会输出堆栈, Debug()/Info() 这些级别不会。运行结果:

{"level":"warn","ts":1587740883.4965692,"caller":"stacktrace/main.go:13","msg":"hello world","stacktrace":"main.f2\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:13\nmain.f1\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9\nmain.main\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22\nruntime.main\n\tC:/Go/src/runtime/proc.go:203"}

stacktrace 单独拉出来:

main.f2
d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:13
  main.f1
  d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9
    main.main
    d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22
      runtime.main
      C:/Go/src/runtime/proc.go:203

很清楚地看到调用路径。

全局 Logger

为了方便使用, zap 提供了两个全局的 Logger ,一个是 *zap.Logger ,可调用 zap.L() 获得;另一个是 *zap.SugaredLogger ,可调用 zap.S() 获得。需要注意的是,全局的 Logger 默认并不会记录日志!它是一个无实际效果的 Logger 。看源码:

// go.uber.org/zap/global.go
var (
  _globalMu sync.RWMutex
  _globalL  = NewNop()
  _globalS  = _globalL.Sugar()
)

我们可以使用 ReplaceGlobals(logger *Logger) func()logger 设置为全局的 Logger ,该函数返回一个无参函数,用于恢复全局 Logger 设置:

func main() {
  zap.L().Info("global Logger before")
  zap.S().Info("global SugaredLogger before")

  logger := zap.NewExample()
  defer logger.Sync()

  zap.ReplaceGlobals(logger)
  zap.L().Info("global Logger after")
  zap.S().Info("global SugaredLogger after")
}

输出:

{"level":"info","msg":"global Logger after"}
{"level":"info","msg":"global SugaredLogger after"}

可以看到在调用 ReplaceGlobals 之前记录的日志并没有输出。

预设日志字段

如果每条日志都要记录一些共用的字段,那么使用 zap.Fields(fs ...Field) 创建的选项。例如在服务器日志中记录可能都需要记录 serverIdserverName

func main() {
  logger := zap.NewExample(zap.Fields(
    zap.Int("serverId", 90),
    zap.String("serverName", "awesome web"),
  ))

  logger.Info("hello world")
}

输出:

{"level":"info","msg":"hello world","serverId":90,"serverName":"awesome web"}

与标准日志库搭配使用

如果项目一开始使用的是标准日志库 log ,后面想转为 zap 。这时不必修改每一个文件。我们可以调用 zap.NewStdLog(l *Logger) *log.Logger 返回一个标准的 log.Logger ,内部实际上写入的还是我们之前创建的 zap.Logger

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  std := zap.NewStdLog(logger)
  std.Print("standard logger wrapper")
}

输出:

{"level":"info","msg":"standard logger wrapper"}

很方便不是吗?我们还可以使用 NewStdLogAt(l *logger, level zapcore.Level) (*log.Logger, error) 让标准接口以 level 级别写入内部的 *zap.Logger

如果我们只是想在一段代码内使用标准日志库 log ,其它地方还是使用 zap.Logger 。可以调用 RedirectStdLog(l *Logger) func() 。它会返回一个无参函数恢复设置:

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  undo := zap.RedirectStdLog(logger)
  log.Print("redirected standard library")
  undo()

  log.Print("restored standard library")
}

看前后输出变化:

{"level":"info","msg":"redirected standard library"}
2020/04/24 22:13:58 restored standard library

当然 RedirectStdLog 也有一个对应的 RedirectStdLogAt 以特定的级别调用内部的 *zap.Logger 方法。

总结

zap 用在日志性能和内存分配比较关键的地方。本文仅介绍了 zap 库的基本使用,子包 zapcore 中有更底层的接口,可以定制丰富多样的 Logger

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

参考

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

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

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

Go 每日一库之 zap

欢迎关注我们的微信公众号,每天学习Go知识

Go 每日一库之 zap

以上所述就是小编给大家介绍的《Go 每日一库之 zap》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

我看电商3:零售的变革

我看电商3:零售的变革

黄若 / 电子工业出版社 / 2018-4 / 49

在《我看电商3:零售的变革》之前,黄若先生的“我看电商”系列图书《我看电商》《再看电商》《我看电商2》,均为行业畅销书。黄若先生的图书有两大特如一是干货满满,二是观点鲜明。 “新零售”是眼下的热门词。在2017年里,数以万计的企业以“新零售”作为标识进入市场。但是社会上对“新零售“存在着各种模糊的定义和不尽相同的解读。 《我看电商3:零售的变革》中明确提出:新零售不应过分关注于渠道形式......一起来看看 《我看电商3:零售的变革》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具