Skip to main content

Zap

Zap是Uber开源的日志记录包:

  1. 高性能。
  2. 日志等级封装很科学。
  3. 支持自定义日志格式。
  4. 支持公共字段定义。
  5. 支持调用栈信息输出。
  6. 支持日志命名空间。
  7. 支持钩子操作。

日志分类

Zap中日志可分为两种:

  • 标准Logger

    • 日志相关字段是强类型。
    • 性能非常高。
    • 更加安全。
  • SugaredLogger

    • 配置字段是弱类型且是分散的。
    • 性能略低于标准Logger。
    • 安全性略低。
    • 支持Printf

日志模式

Zap创建Logger对象的方式有三种:

开发模式

开发模式使用zap.NewDevelopment方法创建logger,此模式下会输出标准logger给谁。

package main

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

func main() {
logger, err := zap.NewDevelopment()

if err != nil {
log.Fatal("Logger create failed.")
return
}

// flushes buffer
defer logger.Sync()

// 2024-08-24T21:43:44.462+0800 WARN basic2/main.go:22 This is a warn log
logger.Warn("This is a warn log")
}

生产模式

生产模式使用zap.NewProduction方法创建logger,此模式下会输出标准JSON格式的字符串。

package main

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

func main() {
logger, err := zap.NewProduction()

if err != nil {
log.Fatal("Logger create failed.")
return
}

// flushes buffer
defer logger.Sync()

// {"level":"warn","ts":1724508501.310736,"caller":"basic2/main.go:22","msg":"This is a warn log"}
logger.Warn("This is a warn log")
}

示例模式

示例模式使用zap.NewExample方法创建logger,此模式下没有时间、文件位置,只有等级和信息,输出一个标准JSON格式字符串。

package main

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

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

// flushes buffer
defer logger.Sync()

// {"level":"warn","msg":"This is a warn log"}
logger.Warn("This is a warn log")
}

日志等级

  1. Debug 调试日志(生产模式下不包含)。
  2. Info 普通日志。
  3. Warn 警告日志。
  4. Error 普通错误日志。错误事件,只是一个普通的错误,程序可以继续运行。
  5. DPanic 软Panic错误日志。软错误,输出错误栈信息,程序不会终止。
  6. Panic 硬Panic错误日志。严重错误,输出错误栈信息,程序会终止。
  7. Fatal 严重错误日志。严重错误,不会输出错误栈信息,程序会终止。
logger.Debug("This is a debug log")
logger.Info("This is a info log")
logger.Warn("This is a warn log")
logger.Error("This is a error log")
logger.DPanic("This is a dpanic log")
logger.Panic("This is a panic log")
logger.Fatal("This is a fatal log")

SugaredLogger

Suger与标准Logger的主要区别是可进行松散型的传参方式,且是弱类型。

su := logger.Sugar()

// 松散型传值
su.Warnw("Failed to connect to database:",
"IP", "192.168.101.156",
"PORT", 3306,
)

logger.Warn("Failed to connect to database",
zap.String("IP", "192.168.101.156"),
zap.Int("PORT", 3306),
)

Hooks

Hook是一个函数,当一次日志记录完毕后,zap会自动调用Hook函数,Hook函数可以有多个。

package main

import (
"fmt"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
logger, _ := zap.NewProduction(zap.Hooks(func(e zapcore.Entry) error {
fmt.Println("Entry1")
return nil
}, func(e zapcore.Entry) error {
fmt.Println("Entry2")
return nil
}))

logger.Info("Info log")
logger.Error("Error log")
}
/*
{"level":"info","ts":1724552778.859971,"caller":"basic2/main.go:55","msg":"Info log"}
Entry1
Entry2
{"level":"error","ts":1724552778.860116,"caller":"basic2/main.go:56","msg":"Error log","stacktrace":"main.main\n\t/Users/jason/code/go-dev/log/basic2/main.go:56\nruntime.main\n\t/opt/homebrew/Cellar/go/1.23.0/libexec/src/runtime/proc.go:272"}
Entry1
Entry2
*/

自定义配置

zap可对日志自定义配置:

package main

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

func main() {
// json配置
jsonStream := []byte(`
{
"level": "debug",
"encoding": "json",
"outputPaths": ["stdout", "./logs/log-info.log"],
"errorOutputPaths": ["stderr", "./logs/log-error.log"],
"encoderConfig": {
"messageKey": "msg",
"levelKey": "level",
"levelEncoder": "captial"
}
}
`)

var config zap.Config

// 将json配置解析zap.Config
if err := json.Unmarshal(jsonStream, &config); err != nil {
panic(err.Error())
}

// 使用Must创建及Build方法创建logger
logger := zap.Must(config.Build())

defer logger.Sync()

logger.Info("This is a info log")
logger.Error("This is a error log")
}

由于配置时使用到字符串并不好维护,因此实际开发中并不常用。

分文件

在记录日志时,可将不同的日志按等级写入不同的日志文件,便于查找和分析。

分文件后,当前的等级文件会写入当前日志等级及当前日志等级以上等级的日志,如Error等级的文件会写入ErrorDPanicPanicFatal等级的日志。

package main

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

const (
fileFlag = os.O_CREATE | os.O_APPEND | os.O_RDWR
perm = 0666
)

func main() {
config := zap.NewProductionEncoderConfig()

config.EncodeTime = zapcore.ISO8601TimeEncoder

fileEncoder := zapcore.NewConsoleEncoder(config)

fileMap := map[string]string{
"DEBUG": "./logs/log-debug.log",
"INFO": "./logs/log-info.log",
"WARN": "./logs/log-warn.log",
"ERROR": "./logs/log-error.log",
"DPANIC": "./logs/log-dpanic.log",
"PANIC": "./logs/log-panic.log",
"FATAL": "./logs/log-fatal.log",
}

debugFile, _ := os.OpenFile(fileMap["DEBUG"], fileFlag, perm)
infoFile, _ := os.OpenFile(fileMap["INFO"], fileFlag, perm)
warnFile, _ := os.OpenFile(fileMap["WARN"], fileFlag, perm)
errorFile, _ := os.OpenFile(fileMap["ERROR"], fileFlag, perm)
dpanicFile, _ := os.OpenFile(fileMap["DPANIC"], fileFlag, perm)
panicFile, _ := os.OpenFile(fileMap["PANIC"], fileFlag, perm)
fatalFile, _ := os.OpenFile(fileMap["FATAL"], fileFlag, perm)

teeCore := zapcore.NewTee(
zapcore.NewCore(fileEncoder, zapcore.AddSync(debugFile), zap.DebugLevel),
zapcore.NewCore(fileEncoder, zapcore.AddSync(infoFile), zap.InfoLevel),
zapcore.NewCore(fileEncoder, zapcore.AddSync(warnFile), zap.WarnLevel),
zapcore.NewCore(fileEncoder, zapcore.AddSync(errorFile), zap.ErrorLevel),
zapcore.NewCore(fileEncoder, zapcore.AddSync(dpanicFile), zap.DPanicLevel),
zapcore.NewCore(fileEncoder, zapcore.AddSync(panicFile), zap.PanicLevel),
zapcore.NewCore(fileEncoder, zapcore.AddSync(fatalFile), zap.FatalLevel),
)

logger := zap.New(teeCore, zap.AddCaller())

defer logger.Sync()

logger.Debug("This is a Debug log")
logger.Info("This is a Info log")
logger.Warn("This is a Warn log")
logger.Error("This is a Error log")
logger.DPanic("This is a DPanic log")
logger.Panic("This is a Panic log")
logger.Fatal("This is a Fatal log")
}

命名空间

在记录日志时,可使用Namespace方法增加命名空间,对信息集合进行分类,方便进行调试和维护:

// 2024-08-25T11:49:41.442+0800	info basic3/main.go:55 This is a Info log {"namespace name": {"key1": "value1", "key2": "value2"}}
logger.Info("This is a Info log",
zap.Namespace("namespace name"),
zap.String("key1", "value1"),
zap.String("key2", "value2"),
)

分割与备份

日志分割指将日志文件按照一定的策略进行切分,以防止单个日志文件过大,影响系统性能或磁盘空间使用。

日志备份是指将已生成的日志文件保存到一个安全的位置,以确保日志数据的完整性,并在需要时进行恢复。

在进行日志分割与备份时,需要考虑:

  • 一个文件最多保存多大的日志(MaxSize)。
  • 最多可备份最近多少个日志文件(MaxBackups)。
  • 日志文件最多保存多久(MaxAge)。
  • 日志文件是否需要压缩(Compress)。

Zap中,可借助lumberjack来进行日志分割与备份:

import (
"gopkg.in/natefinch/lumberjack.v2"
)

infoFile := &lumberjack.Logger{
Filename: "./logs/log-info.log",
MaxSize: 1, // MB
MaxBackups: 3, // 最多保存最近三个文件
MaxAge: 30, // days
Compress: true, // 开启压缩
}

全局Logger

Zap中有两大logger类型:

  • New创建的logger对象,利用logger对象来输出日志。
  • zap.L()或者zap.S()创建标准loggerSugeredLogger
package main

import "go.uber.org/zap"

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

zap.ReplaceGlobals(logger)

zap.L().Info("This is a info log")
zap.S().Warn("This is a suger warn log")
}

logger封装

经过封装,可以使用全局logger,也可使用单独的logger

package main

import (
"basic3/utils"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
logger := utils.NewLogger(utils.LoggerConfig{
Mode: "PRD",
FilePath: "./log/log-warn.log",
MaxSize: 1,
MaxBackups: 3,
MaxAge: 30,
Level: zapcore.WarnLevel,
})

logger.Warn("This is a warning log")
zap.L().Warn("This is an another warning log")
}