日志库 —— Go Zap 入门

170 阅读4分钟

本文通过丰富的代码示例演示了 zap 的基本功能,读者可以跟随这些示例,学习如何使用 zap

Go Zap

Go zap 是一个用 Go 语言编写的快速、结构化、分级的日志库。

安装

go get -u go.uber.org/zap

注意,zap 仅支持 Go 的两个最新小版本。

选择 Logger

在性能要求较高但并不重要的情况下,可以使用 SugaredLogger。它比其他结构化日志程序包快 4-10 倍,并包含结构化和 printf 风格的 API。默认情况下,日志记录器是无缓冲的。不过,由于 zap 的底层 API 允许缓冲,因此在进程退出前调用 Sync 是个好习惯。

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

	sugar := logger.Sugar()

	sugar.Infow("failed to fetch URL",
		"url", "https://www.google.com",
		"attempt", 3,
		"backoff", time.Second,
	)
	
	sugar.Infof("Failed to fetch URL: %s", "https://www.google.com")
}
go run main.go
#{"level":"info","ts":1692068155.4790313,"caller":"zap-demo/main.go:14","msg":"failed to fetch URL","url":"https://www.google.com","attempt":3,"backoff":1}
#{"level":"info","ts":1692068155.4790313,"caller":"zap-demo/main.go:19","msg":"Failed to fetch URL: https://www.google.com"}

在每一微秒和每次分配都很重要的罕见情况下,请使用 Logger。它比 SugaredLogger 更快,分配的资源也少得多,但只支持强类型、结构化日志记录,如:

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

	logger.Info("failed to fetch URL",
		zap.String("url", "https://www.google.com"),
		zap.Int("attempt", 3),
		zap.Duration("backoff", time.Second),
	)
}
go run main.go
#{"level":"info","ts":1692069088.4173353,"caller":"zap-demo/main.go:12","msg":"failed to fetch URL","url":"https://www.google.com","attempt":3,"backoff":1}

LoggerSugaredLogger 之间做出选择并不需要在整个应用程序中进行:在两者之间进行转换既简单又便宜,如:

func main() {
	logger := zap.NewExample()
	defer logger.Sync()
	
	sugar := logger.Sugar()
	
	plain := sugar.Desugar()
}

配置 Zap

构建记录仪的最简单方法是使用 zap 的固定预设:NewExampleNewProductionNewDevelopment。这些预设通过单个函数调用构建一个记录器,它们的说明和用法如下:

  • NewExample 是用于演示和测试 zap 的功能的预设。它会将日志以人类可读的格式输出到标准输出,并且包含所有日志级别和上下文信息。它不会对日志进行采样或添加调用者信息,也不会使用任何编码器配置。

    func main() {
    	logger := zap.NewExample()
    	defer logger.Sync()
    
    	logger.Info("failed to fetch URL",
    		zap.String("url", "https://www.google.com"),
    		zap.Int("attempt", 3),
    		zap.Duration("backoff", time.Second),
    	)
    }
    
    go run main.go
    #{"level":"info","msg":"failed to fetch URL","url":"https://www.google.com","attempt":3,"backoff":"1s"}
    
  • NewProduction 是用于生产环境的预设。它会将日志以 JSON 格式输出到标准错误,并且只包含 InfoLevel 或更高级别的日志。它会对日志进行采样,以减少性能开销和磁盘占用。它还会添加调用者信息,以便追踪日志来源。它还会使用一个默认的生产环境编码器配置,包括时间戳、日志级别、消息等字段。

    func main() {
    	logger, _ := zap.NewProduction()
    	defer logger.Sync()
    
    	logger.Info("failed to fetch URL",
    		zap.String("url", "https://www.google.com"),
    		zap.Int("attempt", 3),
    		zap.Duration("backoff", time.Second),
    	)
    }
    
    go run main.go
    #{"level":"info","ts":1692070272.7691774,"caller":"zap-demo/main.go:12","msg":"failed to fetch URL","url":"https://www.google.com","attempt":3,"backoff":1}
    
  • NewDevelopment 是用于开发环境的预设。它会将日志以人类可读的格式输出到标准错误,并且包含所有日志级别和上下文信息。它不会对日志进行采样,以便查看所有日志细节。它也会添加调用者信息,以及堆栈跟踪信息(如果有)。它还会使用一个默认的开发环境编码器配置,包括颜色化的时间戳、日志级别、消息等字段。

    func main() {
    	logger, _ := zap.NewDevelopment()
    	defer logger.Sync()
    
    	logger.Info("failed to fetch URL",
    		zap.String("url", "https://www.google.com"),
    		zap.Int("attempt", 3),
    		zap.Duration("backoff", time.Second),
    	)
    }
    
    go run main.go
    #2023-08-15T11:32:45.372+0800    INFO    zap-demo/main.go:12     failed to fetch URL     {"url": "https://www.google.com", "attempt": 3, "backoff": "1s"}
    

预设对于小型项目来说没有问题,但大型项目和组织自然需要更多的定制。对于大多数用户来说,zap 的 Config 结构在灵活性和便利性之间取得了恰当的平衡。

func main() {
	config := zap.Config{
		Level:       zap.NewAtomicLevelAt(zap.DebugLevel), // set the minimum enabled log level
		Development: false,                                // set the development/production mode
		Encoding:    "console",                            // set the encoding format (console or json)

		EncoderConfig: zapcore.EncoderConfig{
			MessageKey:   "msg",                            // set the key for the message field
			LevelKey:     "level",                          // set the key for the level field
			TimeKey:      "time",                           // set the key for the time field
			CallerKey:    "caller",                         // set the key for the caller field
			EncodeLevel:  zapcore.CapitalColorLevelEncoder, // set the level encoder (capitalized and colorized)
			EncodeTime:   zapcore.ISO8601TimeEncoder,       // set the time encoder (ISO 8601 format)
			EncodeCaller: zapcore.ShortCallerEncoder,       // set the caller encoder (short file name and line number)
		},

		OutputPaths:      []string{"stdout"},                           // set the output paths for the logs
		ErrorOutputPaths: []string{"stderr"},                           // set the output paths for the errors
		InitialFields:    map[string]interface{}{"app": "zap-example"}, // set the initial fields for the logs
	}

	logger, _ := config.Build()
	defer logger.Sync()

	logger.Info("failed to fetch URL",
		zap.String("url", "https://www.google.com"),
		zap.Int("attempt", 3),
		zap.Duration("backoff", time.Second),
	)
}
go run main.go
#2023-08-15T11:55:52.980+0800    INFO    zap-demo/main.go:33     failed to fetch URL     {"app": "zap-example", "url": "https://www.google.com", "attempt": 3, "backoff": 1000000000}

更特殊的配置(在文件间分割输出、将日志发送到消息队列等)也是可能的,但需要直接使用 go.uber.org/zap/zapcore,如:

func customLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
	switch level {
	case zapcore.DebugLevel:
		enc.AppendString("D")
	case zapcore.InfoLevel:
		enc.AppendString("I")
	case zapcore.WarnLevel:
		enc.AppendString("W")
	case zapcore.ErrorLevel:
		enc.AppendString("E")
	case zapcore.DPanicLevel:
		enc.AppendString("P")
	case zapcore.PanicLevel:
		enc.AppendString("P")
	case zapcore.FatalLevel:
		enc.AppendString("F")
	default:
		enc.AppendString("?")
	}
}

func main() {
	errorFile, _ := os.Create("error.log")
	defer errorFile.Close()

	infoFile, _ := os.Create("info.log")
	defer infoFile.Close()

	customEncoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
		MessageKey:       "msg",
		LevelKey:         "level",
		TimeKey:          "time",
		CallerKey:        "caller",
		EncodeLevel:      customLevelEncoder,
		EncodeTime:       zapcore.ISO8601TimeEncoder,
		EncodeCaller:     zapcore.ShortCallerEncoder,
		ConsoleSeparator: " | ",
	})

	errorCore := zapcore.NewCore(
		customEncoder,
		zapcore.AddSync(errorFile),
		zap.ErrorLevel,
	)

	infoCore := zapcore.NewCore(
		zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
		zapcore.AddSync(infoFile),
		zap.InfoLevel,
	)

	logger := zap.New(zapcore.NewTee(
		errorCore,
		infoCore,
	))
	defer logger.Sync()

	logger.Error("failed to fetch URL",
		zap.String("url", "https://www.google.com"),
		zap.Int("attempt", 3),
		zap.Duration("backoff", time.Second),
	)

	logger.Info("failed to fetch URL",
		zap.String("url", "https://www.google.com"),
		zap.Int("attempt", 3),
		zap.Duration("backoff", time.Second),
	)
}

执行命令 go run main.go,项目根目录下会创建两个文件 error.loginfo.log,它们的内容如下:

// error.log
2023-08-15T13:09:10.778+0800 | E | failed to fetch URL | {"url": "https://www.google.com", "attempt": 3, "backoff": 1000000000}

// info.log
{"level":"error","ts":1692076150.7782364,"msg":"failed to fetch URL","url":"https://www.google.com","attempt":3,"backoff":1}
{"level":"info","ts":1692076150.793304,"msg":"failed to fetch URL","url":"https://www.google.com","attempt":3,"backoff":1}

本文由 Sue211213 原创,发表于 2023 年 8 月 15 日,转载请注明出处和作者,谢谢!