📅 2024年11月26日

📦 go1.21.5zap

看到工作使用的是 zap顺手就学一下

🏆 ZAP

在`zap`的`github`主页给出了一个数据表格,用来对比其他go的日志框架的速度,毋庸置疑zap肯定是最快的,具体可以去`github`上查看

github: https://github.com/uber-go/zap

为什么它会这么快了呢?在官方文档中也说了: `Zap` 采用了不同的方法。它包含一个无反射、零分配的 `JSON` 编码器,并且基本 Logger 力求尽可能避免序列化开销和分配。通过在此基础上构建高级 `SugaredLogger`,`zap` 让用户可以选择何时需要计算每个分配以及何时更喜欢更熟悉的松散类型 API。

🍪 开始

首先当然是安装,可以直接通过 go get安装

go get -u go.uber.org/zap
安装完成后就可以直接导入使用,此外zap提供两个日志记录器分别是`sugaredlogger`和`logger`(但是我感觉差别不大),两者差异如下:

1.在性能要求较高但又不重要的环境中,请使用 SugaredLogger。它比其他结构化日志包快 4-10 倍,并且支持结构化和 printf 样式的日志记录。与 log15go-kit 一样,SugaredLogger 的结构化日志 API 是弱类型的,并接受可变数量的键值对。

func main() {
	sugar := zap.NewExample().Sugar()
    // 程序结束Sync同步缓冲
	defer sugar.Sync()
	sugar.Infow("无法获取 URL",
		"url", "http://example.com",
		"attempt", 3,
		"backoff", time.Second,
	)
	// 类似于prinf 
	sugar.Infof("无法获取 URL: %s", "http://example.com")
}

// 输出:
{"level":"info","msg":"无法获取 URL","url":"http://example.com","attempt":3,"backoff":"1s"}
{"level":"info","msg":"无法获取 URL: http://example.com"}

2.在极少数情况下,每微秒和每次分配都很重要,请使用 Logger。它甚至比 SugaredLogger 更快,分配的资源也少得多,但它仅支持强类型、结构化日志记录

func main() {
	logger := zap.NewExample()
	defer logger.Sync()
	logger.Info("failed to fetch URL",
		zap.String("url", "http://example.com"),
		zap.Int("attempt", 3),
		zap.Duration("backoff", time.Second),
	)
}

3.在 sugaredlogger 之间转换也很方便

logger := zap.NewExample()
defer logger.Sync()
sugar := logger.Sugar()
plain := sugar.Desugar()

4.创建 logger的方法有很多主要有一下三种

zap.NewExample()
zap.NewDevelopment()
zap.NewProduction()

使用 NewExample 构建了一个 Logger ,该 Logger 专为在 zap 的可测试示例中使用而设计。它将 DebugLevel 及以上日志作为 JSON 写入标准输出,但省略了时间戳和调用函数,以保持示例输出简短且具有确定性

使用 NewDevelopment 构建了一个开发 LoggerNewDevelopment会生成包含调用者信息、堆栈跟踪等详细信息的日志,而且它输出是带有彩色的

使用 NewProduction 构建了一个合理的生产 Logger ,它将 InfoLevel 及以上的日志作为 JSON 写入标准错误,它输出也是彩色的

可见他们的区别在于写入的日志等级和写入日志的格式,测试一下代码如下:

func main() {
	example := zap.NewExample()
	development, _ := zap.NewDevelopment()
	production, _ := zap.NewProduction()
	defer func() {
		example.Sync()
		development.Sync()
		production.Sync()
	}()
	example.Info("hello world")
	development.Info("hello world")
	production.Info("hello world")
}

// 输出如下

image-20241126214442159

可以看到如果使用`Newexample`那么就是简简单单的一个日志,production和development都输出了时间,前者是时间戳,后者格式化了,而`development` 输出更加简洁

那么如果说`production`的日志遇到`debug`会怎么样? **答案是不会输出**

1️⃣ 日志等级

logrus差不多,但是少了一个 trace 多了一个 DPanic请查看:Logrus 安装

  • Panic:记录日志,然后 panic
  • Fatal:致命错误,出现错误时程序无法正常运转。输出日志后,程序退出;
  • Error:错误日志,需要查看原因;
  • Warn:警告信息,提醒程序员注意;
  • Info:关键操作,核心流程的日志;
  • Debug:一般程序中输出的调试信息;
func main() {
	logger := zap.NewExample().Sugar()
	defer logger.Sync()
	logger.Debug("Debug Test")
	logger.Info("Info Test")
	logger.Warn("Warn Test")
	logger.Fatal("Fatal Test") // 此处就直接发生报错就不会在输出下面的语句了
	logger.Panic("Panic Test")
}

下面是 zap的日志等级,可以看到有个 DPanicPanic,我本来也很困惑测试了一下之后发现确实不同

const (
	// DebugLevel logs are typically voluminous, and are usually disabled in
	// production.
	DebugLevel = zapcore.DebugLevel
	// InfoLevel is the default logging priority.
	InfoLevel = zapcore.InfoLevel
	// WarnLevel logs are more important than Info, but don't need individual
	// human review.
	WarnLevel = zapcore.WarnLevel
	// ErrorLevel logs are high-priority. If an application is running smoothly,
	// it shouldn't generate any error-level logs.
	ErrorLevel = zapcore.ErrorLevel
	// DPanicLevel logs are particularly important errors. In development the
	// logger panics after writing the message.
	DPanicLevel = zapcore.DPanicLevel
	// PanicLevel logs a message, then panics.
	PanicLevel = zapcore.PanicLevel
	// FatalLevel logs a message, then calls os.Exit(1).
	FatalLevel = zapcore.FatalLevel
)

DPanic方法也用于记录错误日志,但与 Panic不同的是,它不会触发程序的 panicDPanic方法会在记录日志前检查是否启用了调试模式(通过 zapcore.WriteSyncerCheck方法)。如果启用了调试模式,DPanic会触发 panic并输出堆栈跟踪信息;否则,它只会记录错误日志,程序会继续执行。

DPanic方法通常用于处理可恢复的错误,或者在开发环境中记录详细的错误信息。在生产环境中,可以通过配置禁用 DPanic的panic行为,以避免不必要的程序中断,测试一下

💻 DPanic

在输出 DPanic信息中使用 development和其它两种方法不一样,下面是 developmentDPanic输出,也就是说 development默认开启了调试模式

func main() {
	development, _ := zap.NewDevelopment()
	defer development.Sync()
	development.DPanic("DPanic")
}



//输出
2024-11-26T22:06:41.096+0800	DPANIC	Gin_zap/main.go:18	DPanic
main.main
	D:/learnCode/Go/GinStudy/Gin_zap/main.go:18
runtime.main
	C:/Program Files/Go/src/runtime/proc.go:267
panic: DPanic

goroutine 1 [running]:
go.uber.org/zap/zapcore.CheckWriteAction.OnWrite(0x0?, 0x0?, {0x0?, 0x0?, 0xc0000604e0?})
	C:/Users/tanchang/go/pkg/mod/go.uber.org/zap@v1.27.0/zapcore/entry.go:196 +0x54
go.uber.org/zap/zapcore.(*CheckedEntry).Write(0xc000020a90, {0x0, 0x0, 0x0})
	C:/Users/tanchang/go/pkg/mod/go.uber.org/zap@v1.27.0/zapcore/entry.go:262 +0x3ec
go.uber.org/zap.(*Logger).DPanic(0x0?, {0xc2124c?, 0x0?}, {0x0, 0x0, 0x0})
	C:/Users/tanchang/go/pkg/mod/go.uber.org/zap@v1.27.0/logger.go:275 +0x51
main.main()
	D:/learnCode/Go/GinStudy/Gin_zap/main.go:18 +0x65

此外你看到了输出了一条 DPanic日志但是还是打印出来堆栈的信息了,但是如果你在 DPanic后面在接一段程序代码,它不会继续执行

func main() {
	development, _ := zap.NewDevelopment()
	defer development.Sync()
	development.DPanic("我发生Panic啦!救命啊!")
	fmt.Println("嘿嘿骗你的,我还可以运行")
}


//输入和上面一样并不会执行下面的println语句

那么查看一下其他两种,没有开启调试模式的,example就和普通日志差不多还可以继续,但是 production会输出一点栈 trace,但是也不会终止执行

func main() {
	production, _ := zap.NewProduction()
	example := zap.NewExample()
	defer func() {
		production.Sync()
		example.Sync()
	}()
	example.DPanic("我发生Panic啦!救命啊!")
	fmt.Println("嘿嘿骗你的,我还可以运行")
	production.DPanic("我真发生Panic啦!救命啊!")
	println("test")
}

//
{"level":"dpanic","msg":"我发生Panic啦!救命啊!"}
嘿嘿骗你的,我还可以运行
{"level":"dpanic","ts":1732631310.1716707,"caller":"Gin_zap/main.go:25","msg":"我真发生Panic啦!救命啊!","stacktrace":"main.main\n\tD:/learnCode/Go/GinStudy/Gin_zap/main.go:25\nruntime.main\n\tC:/Program Files/Go/src/runtime/proc.go:267"}
test

2️⃣ 使日志有有层级

类似于`json`,或者说是`python`内字典嵌套字典

我想象的场景就是你在初始化的时候可能需要链接不同的服务,最后链接结果可以总结道一条日志然后返回如下演示,使用`with`方法是创建一个带有子日志的行`logger`
func main() {
	logger := zap.NewExample()
	defer logger.Sync()

	serviceConnlog := logger.With(
		zap.Namespace("service"),
		zap.String("redis", "链接失败"),
		zap.String("mysql", "链接失败"),
	)
	serviceConnlog.Error("服务出错")
}


//输出
{"level":"error","msg":"服务出错","service":{"redis":"链接失败","mysql":"链接失败"}}

当然可以直接在 Errof直接写

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

	logger.Error("服务启动失败",
		zap.Namespace("service"),
		zap.String("redis", "链接失败"),
		zap.String("mysql", "链接失败"),
	)
}

3️⃣ 对 logger定制

使用配置文件配置

在官方文档说: 预设适用于小型项目,但大型项目和组织自然需要更多的自定义。对于大多数用户来说,`zap` 的 `Config` 结构体在灵活性和便利性之间取得了适当的平衡。可以直接使用预设置的方法来使用`zap`,也可以使用`config`结构体来创建配置。

Config结构体如下

type Config struct {
	// 设置日志级别
	Level AtomicLevel `json:"level" yaml:"level"`
	// 开启开发模式,启用堆栈追踪
	Development bool `json:"development" yaml:"development"`
	// 禁用调用者信息,不显示日志的文件名和行号
	DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
	// 完全禁用自动堆栈跟踪捕获
	DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
	// 设置采样策略
	Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
	// 设置日志编码方式
	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"`
}

其中有个 EncodingConfig

type EncoderConfig struct {
	// Set the keys used for each log entry. If any key is empty, that portion
	// of the entry is omitted.
	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"`
	FunctionKey    string `json:"functionKey" yaml:"functionKey"`
	StacktraceKey  string `json:"stacktraceKey" yaml:"stacktraceKey"`
	SkipLineEnding bool   `json:"skipLineEnding" yaml:"skipLineEnding"`
	LineEnding     string `json:"lineEnding" yaml:"lineEnding"`
	// Configure the primitive representations of common complex types. For
	// example, some users may want all time.Times serialized as floating-point
	// seconds since epoch, while others may prefer ISO8601 strings.
	EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
	EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
	EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
	EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
	// Unlike the other primitive type encoders, EncodeName is optional. The
	// zero value falls back to FullNameEncoder.
	EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
	// Configure the encoder for interface{} type objects.
	// If not provided, objects are encoded using json.Encoder
	NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
	// Configures the field separator used by the console encoder. Defaults
	// to tab.
	ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

其中主要的配置有:

  • MessageKey:日志中信息的键名,默认为 msg
  • LevelKey:日志中级别的键名,默认为 level
  • EncodeLevel:日志中级别的格式,默认为小写,如 debug/info

接下来定制一下

func main() {
	zapConfig := []byte(`{
	// 日志等级为debug
	  "level": "debug",
	  "encoding": "json",
	  // 错误和正常日志输出
	  "outputPaths": ["stdout"],
	  "errorOutputPaths": ["stderr"],
	  // 初始化日志输出头
	  "initialFields": {"package": "gin"},
	  "encoderConfig": {
	    "messageKey": "message",
	    "levelKey": "level",
	    "levelEncoder": "lowercase",
        "timeKey": "time",
		"timeEncoder": "iso8601"
	  }
	}`)

	// 解析为json
	var zapcfg zap.Config
	if err := json.Unmarshal(zapConfig, &zapcfg); err != nil {
		panic(err)
	}

	// 使用build通过自定义设置创建logger
	newLogger := zap.Must(zapcfg.Build())
	defer newLogger.Sync()
	newLogger.Info("这是一条成功消息")
	newLogger.Error("这是一条ERRO消息")
}

//输出:
{"level":"info","time":"2024-11-29T21:28:53.448+0800","message":"这是一条成功消息","package":"gin"}
{"level":"error","time":"2024-11-29T21:28:53.479+0800","message":"这是一条ERRO消息","package":"gin"}

使用option配置

此外在使用 NewProduction/... 这几种默认方法创建 logger时可以传递多个 option这些 option也可以配置 logger,比如 zap.AddCaller()可以输出当前文件目录和代码行数

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

	production.Info("test")
}

//out
{"level":"info","ts":1732887442.3083456,"caller":"Gin_zap/main.go:17","msg":"test"}

还可以使用 zap.AddCallerSkip()来输出上几层调用代码所在的文件信息

还有 zap.AddStackTrace可以设置哪个等级之上会输出堆栈信息

zap.Fields 可以设置日志字段

4️⃣ 全局 Logger

全局的 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")
}


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