# 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配置，然后暴露统一的日志打印方法，既可以与日志框架分离，又可以集中控制日志配置


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yangsx95.gitbook.io/notes/programming-language/golang/ri-zhi-kuang-jia/zap-ri-zhi-kuang-jia.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
