> For the complete documentation index, see [llms.txt](https://yangsx95.gitbook.io/notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yangsx95.gitbook.io/notes/programming-language/golang/ri-zhi-kuang-jia/zap-ri-zhi-kuang-jia.md).

# zap日志框架

github：[zap](https://github.com/uber-go/zap)

## Hello World

```go
package main

import (
	"time"

	"go.uber.org/zap"
)

func main() {
	logger := zap.NewExample()
	defer logger.Sync() // zap底层 API 可以设置缓存，所以一般使用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),
	)
}
```

由于`fmt.Printf`之类的方法大量使用`interface{}`和反射，会有不少性能损失，并且增加了内存分配的频次。`zap`为了提高性能、减少内存分配次数，没有使用反射，而且默认的`Logger`只支持强类型的、结构化的日志。必须使用`zap`提供的方法记录字段。

比如上述代码中的：

```go
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
```

`zap`为 Go 语言中所有的基本类型和其他常见类型都提供了方法。这些方法的名称也比较好记忆，`zap.Type`（`Type`为`bool/int/uint/float64/complex64/time.Time/time.Duration/error`等）就表示该类型的字段，`zap.Typep`以`p`结尾表示该类型指针的字段，`zap.Types`以`s`结尾表示该类型切片的字段。如：

* `zap.Bool(key string, val bool) Field`：`bool`字段
* `zap.Boolp(key string, val *bool) Field`：`bool`指针字段
* `zap.Bools(key string, val []bool) Field`：`bool`切片字段

其他特殊类型：

* `zap.Any(key string, value interface{}) Field`：任意类型的字段
* `zap.Binary(key string, val []byte) Field`：二进制串的字段

每个字段都使用方法包裹一层比较繁琐，所以zap提供了`SugaredLogger`，使用`logger.Sugar()`即可创建`SugaredLogger`

## SugaredLogger

`sugared`的含义为糖，`SugaredLogger`的含义就是对`Logger`包装了一层语法糖，内部提供两种后缀的方法：

* `Infow`，字段形式输出
* `Infof`，`printf`方式输出

如下：

```go
package main

import (
	"time"

	"go.uber.org/zap"
)

func main() {
	logger := zap.NewExample()
	defer logger.Sync() // zap底层 API 可以设置缓存，所以一般使用defer logger.Sync()将缓存同步到文件中

	sugarLogger := logger.Sugar()
	url := "http://example.org/api"

	// 字段方式输出
	sugarLogger.Infow("failed to fetch URL",
		"url", url,
		"attempt", 3,
		"backoff", time.Second)

	// printf 方式输出
	sugarLogger.Infof("failed to fetch URL %s, %d, %d", url, 3, time.Second)

}
```

输出结果：

```json
{"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, 3, 1000000000"}
```

## 日志级别

zap共提供5中日志级别，分别为：

* `Debug`
* `Info`
* `Warn`
* `Error`，会打印出堆栈信息
* `Fatal`，会打印出堆栈信息，并且调用`os.Exit(1)`退出程序

```go
package main

import (
	"fmt"
	"go.uber.org/zap"
)

func main() {
	var logger *zap.Logger
	logger, _ = zap.NewProduction()

	logger.Debug("i am debug")       // 这行不会打印，因为NewProduction默认日志级别是 INFO
	logger.Info("i am info")         // INFO  级别日志，这个会正常打印
	logger.Warn("i am warn")         // WARN  级别日志，这个会正常打印
	logger.Error("i am error")       // ERROR 级别日志，这个会打印，并附带堆栈信息
	logger.Fatal("i am fatal")       // FATAL 级别日志，这个会打印，附带堆栈信息，并调用 os.Exit 退出
	fmt.Println("can i be printed?") // 这行不会打印，因为程序已经退出
}
```

运行结果：

```json
{"level":"info","ts":1631939779.76289,"caller":"git-knowledge/main.go:13","msg":"i am info"}
{"level":"warn","ts":1631939779.762945,"caller":"git-knowledge/main.go:14","msg":"i am warn"}
{"level":"error","ts":1631939779.762951,"caller":"git-knowledge/main.go:15","msg":"i am error","stacktrace":"main.main\n\t/Users/yangsx/Project/git-knowledge/main.go:15\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:225"}
{"level":"fatal","ts":1631939779.7629771,"caller":"git-knowledge/main.go:16","msg":"i am fatal","stacktrace":"main.main\n\t/Users/yangsx/Project/git-knowledge/main.go:16\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:225"}
```

## 预定义配置Logger

zap提供了三个方法，用于快速创建`logger`：

* `zap.NewExample()`：json格式，debug级别，适合用于测试代码中
* `zap.NewDevelopment()`：console格式，debug级别，人性化显示，用于开发环境
* `zap.NewProduction()`：json格式，info级别，方便解析，用于生产环境
* `zap.NewNop()`：不记录任何日志，直接丢弃

### 选项模式

`NewExample()/NewDevelopment()/NewProduction()`这 3 个函数可以传入若干类型为`zap.Option`的选项，从而定制`Logger`的行为。

### option: 输出文件名以及行号

```go
package main

import (
	"go.uber.org/zap"
)

func main() {
	logger, _ := zap.NewProduction(zap.AddCaller()) // 添加caller 调用位置
	defer logger.Sync()
	// do something
	logger.Info("hello world")
}
```

打印日志：

```json
{"level":"info","ts":1631952102.073369,"caller":"git-knowledge/main.go:11","msg":"hello world"}
```

这里，字段caller打印的调用位置是`main.go:11`行，但是这行一般是打印语句执行的位置，定位到这一行通常没有什么用，需要向前查找。我们可以使用`zap.AddCallerSkip(skip int)`向上跳 1 层。

> 项目中一般会对日志框架调用进行封装，这时候，如果打印的行数是日志的行数，总会带来一定的困惑，所以，可以设置向上跳一层，查看方法调用方的行数，这样比较有用。

### option: 输出调用堆栈

```go
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()
}
```

### option: 预设字段

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

  logger.Info("hello world")
}
```

## 自定义配置Logger

通常情况下，上面的三种配置往往达不到我们的要求，这时通常自定义Logger。使用`zap.Config`结构体创建一个新的Logger配置，并调用他的`Build`方法，构建出一个Logger。

`zap.Config`支持的字段如下：

```go
type Config struct {
	// 日志级别
	Level AtomicLevel `json:"level" yaml:"level"`
	// Development puts the logger in development mode, which changes the
	// behavior of DPanicLevel and takes stacktraces more liberally.
	Development bool `json:"development" yaml:"development"`
	// 禁用caller，也就是日志打印的位置
	DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
	// 禁用堆栈信息
	DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`

	Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
  // Encoding 日志打印的格式，可以是 json 或者 console
	Encoding string `json:"encoding" yaml:"encoding"`
	// Encoding 配置
	EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
	// 输出路径，可以是多个，可以是标准输出 stdout 或者 文件路径
	OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
	// 错误输出路径，可以是多个
	ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
	// 每条日志中都会输出这些值，也就是增加额外的字段
	InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}
```

`EncoderConfig`的具体信息：

```go
type EncoderConfig struct {
  // 日志中信息的键名，默认为msg
  MessageKey    string `json:"messageKey" yaml:"messageKey"`
  // 日志中级别的键名，默认为level
  LevelKey      string `json:"levelKey" yaml:"levelKey"`
  // 日志时间的键名，默认为 ts
  TimeKey       string `json:"timeKey" yaml:"timeKey"`
  // 日志名称的键名，默认为 name
  NameKey       string `json:"nameKey" yaml:"nameKey"`
  // 日志调用位置的键名， 默认为 caller
  CallerKey     string `json:"callerKey" yaml:"callerKey"`
  // 堆栈信息的键名， 默认为 stacktrace
  StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
  // 换行符格式，默认是 \n
  LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
  
  // 日志中级别的格式，默认为小写，如debug/info。 LevelEncoder 是一个闭包函数，以下都是，需要传入相应的实现，来完成encode的操作
  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"`
}
```

此外，针对各种Encoder，zap提供了一些内置的实现：

```go
LevelEncoder:
- zapcore.LowercaseLevelEncoder // 小写
- zapcore.LowercaseColorLevelEncoder // 小写有颜色
- zapcore.CapitalLevelEncoder // 大写
- zapcore.CapitalColorLevelEncoder 大写有颜色

TimeEncoder: 
- zapcore.EpochTimeEncoder // unix 时间戳秒
- zapcore.EpochMillisTimeEncoder // unix时间戳毫秒
- zapcore.EpochNanosTimeEncoder // 纳秒
- zapcore.ISO8601TimeEncoder   // ISO8601格式显示时间
- zapcore.RFC3339TimeEncoder   // RFC3339格式显示时间
- RFC3339NanoTimeEncoder

DurationEncoder:
- SecondsDurationEncoder  // 秒数转换
- NanosDurationEncoder
- MillisDurationEncoder
- StringDurationEncoder

CallerEncoder:
- FullCallerEncoder // /full/path/to/package/file:line
- ShortCallerEncoder // package/file:line


NameEncoder:
- FullNameEncoder
```

## 全局Logger

为了方便在项目中使用，zap提供了两个全局Logger，分别是

* 全局的`*zap.Logger`对象，可以通过`zap.L()`获取
* 全局的`*zap.SugaredLogger`对象，可以通过`zap.S()`获取

注意，这两个Logger默认不记录任何日志，因为他们都是调用`zap.NewNop()`方法生成的，要想使用全局Logger，需要使用`zap.ReplaceGlobals(logger)`函数替换全局Logger：

```go
package main

import (
	"go.uber.org/zap"
)

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")
}
```

输出：

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

> 思考：这里可以使用全局Logger来封装项目的统一Logger。建立一个package，存放全局Logger配置，然后暴露统一的日志打印方法，既可以与日志框架分离，又可以集中控制日志配置
