使用zap打造你的golang日志库

Overview

近期配置了uber家的zap日志库,觉得性能比较强,展示比较美观,在这里做一个分享,代码在第三部分可以自取。

为什么不选择原生log?

说起golang如何优雅的打印日志,任何一个golang的初学者大概都是用的原生log库,或者直接fmt.Println()...但是这种方式并不优雅,并且有以下缺点:

  1. 对于基础日志:不能细粒度区分info和debug级别的日志;
  2. 对于错误日志: 不支持除了fatal或者panic的普通error级别告知。

log示例

 1package main
 2
 3import "log"
 4
 5func main() {
 6    log.Print("info or debug")
 7    log.Fatal("fatal")
 8    log.Panic("panic")
 9}
10
11// 输出如下
122022/11/23 22:23:32 info or debug
132022/11/23 22:23:32 fatal
14exit status 1

为什么不选择logrus?

logrus也是比较常用的自定义日志库,不过因为Go语言是一门强类型的静态语言,而logrus需要知道数据的类型来打印日志,怎么办呢?实现方案是使用反射,这导致大量分配计数。虽然通常不是一个大问题(取决于代码),但是在大规模、高并发的项目中频繁的反射开销影响很大,所以这里不进行采用。

仓库链接: logrus

logrus示例

 1package main
 2
 3import log "github.com/sirupsen/logrus"
 4
 5var logger = log.New()
 6
 7func main() {
 8    // 这里可以通过WithFields来附加字段
 9    logger.WithFields(log.Fields{"testfield": "test"}).Info("test info")
10    logger.Info("info")
11    logger.Error("Error")
12    logger.Fatal("Error")
13    logger.Panic("Panic")
14}
15
16// 输出如下
17INFO[0000] test info                                     testfield=test
18INFO[0000] info
19ERRO[0000] Error
20FATA[0000] Error
21exit status 1

聊一聊zap日志库

仓库链接zap

1. zap支持的六种日志级别

名称 作用
Debug 打印调试时候的debug日志
Info 正常输出普通信息
Warn 警告,可能有部分出现问题但是不影响程序运行
Error 错误,不会中断程序运行但是程序可能已经不正常
Fatal 输出信息,然后调用os.Exit
Panic 调用panic

个人建议如果有错就在初始化检查中可以panic掉,之后程序运行期间就不要碰到小错误就panic掉了,能容忍就抛出Error,这样方便程序员主动停掉服务排查而不是已经工作起来的程序异常挂掉。

2. zap的性能问题及benchmark

官方github在Performance模块中明确说道:

For applications that log in the hot path, reflection-based serialization and string formatting are prohibitively expensive — they're CPU-intensive and make many small allocations. Put differently, using encoding/json and fmt.Fprintf to log tons of interface{}s makes your application slow.

Zap takes a different approach. It includes a reflection-free, zero-allocation JSON encoder, and the base Logger strives to avoid serialization overhead and allocations wherever possible. By building the high-level SugaredLogger on that foundation, zap lets users choose when they need to count every allocation and when they'd prefer a more familiar, loosely typed API.

也就是说,基于反射的序列化,或者字符串格式化这种是很吃cpu资源的,严重了会导致程序变慢(logrus存在这个问题); zap这里定义了一个无反射的,无分配的json encoder来优化这一部分,并且在此基础上提供了Sugar可以舍弃部分性能换取更简单的配置这一特点,我们下面的部分会演示。

通过benckmark可以看出, zap和zap-sugar在性能上还是非常有优势的!

3. 代码展示

下面分段展示完整代码,第一部分是各种包的导入;值得注意的是定义了默认的DefaultLog,用于把Info级别以上的日志人性化可读地输出到控制台; 还支持通过给InitLogger函数传递参数自定义想要的日志形式,支持(假设库名叫logger):

初始化方式 日志形式
logger.DefaultLog 人性化输出到控制台, 输出高于Info级别的日志
logger.InitLogger("", "console", "debug").Sugar() 人性化输出到控制台, 输出高于Debug级别的日志
logger.InitLogger("", "file", "debug").Sugar() 人性化输出到文件(默认当前目录下的log目录,会自动创建), 输出高于Debug级别的日志
logger.InitLogger("json", "console", "debug").Sugar() json格式输出到控制台, 输出高于Debug级别的日志
 1package logger
 2
 3import (
 4	"os"
 5
 6	"github.com/natefinch/lumberjack"
 7	"go.uber.org/zap"
 8	"go.uber.org/zap/zapcore"
 9)
10
11var (
12	MyLogger   = InitLogger()
13	DefaultLog = MyLogger.Sugar()
14)
15
16func InitLogger(logArgs ...string) *zap.Logger {
17    ...
18}

InitLogger函数中大概分为四个部分:

  1. 接收参数初始化变量
  2. 生成encoder
  3. 定义日志级别和输出形式
  4. 根据指定配置生成并返回日志实例
  • 代码
  1func InitLogger(logArgs ...string) *zap.Logger {
  2	var logger *zap.Logger
  3	var coreArr []zapcore.Core
  4	var format, logType, priority string
  5
  6	// get the parameters
  7	switch {
  8	case len(logArgs) >= 3:
  9		format = logArgs[0]
 10		logType = logArgs[1]
 11		priority = logArgs[2]
 12	case len(logArgs) == 2:
 13		format = logArgs[0]
 14		logType = logArgs[1]
 15	case len(logArgs) == 1:
 16		format = logArgs[0]
 17	}
 18
 19	// get encoder
 20	encoderConfig := zap.NewProductionEncoderConfig()
 21	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder        // time format
 22	encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder // use different color for various log levels
 23	// uncomment next line to show full path of the code
 24	// encoderConfig.EncodeCaller = zapcore.FullCallerEncoder
 25	// NewJSONEncoder() for json,NewConsoleEncoder() for normal
 26	if format == "" {
 27		format = "normal"
 28	}
 29	encoder := zapcore.NewConsoleEncoder(encoderConfig)
 30	if format == "json" {
 31		encoder = zapcore.NewJSONEncoder(encoderConfig)
 32	}
 33
 34	// log levels
 35	errorPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
 36		return lev >= zap.ErrorLevel
 37	})
 38	infoPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
 39		return lev < zap.ErrorLevel && lev >= zap.InfoLevel
 40	})
 41	debugPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
 42		return lev < zap.InfoLevel && lev >= zap.DebugLevel
 43	})
 44	if logType == "" {
 45		logType = "console"
 46	}
 47	// writeSyncer for debug file
 48	debugFileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
 49		Filename:   "./log/debug.log", // will create if not exist
 50		MaxSize:    128,               // max size for log file, unit:MB
 51		MaxBackups: 3,                 // max backup's count
 52		MaxAge:     10,                // max reserved days for log file
 53		Compress:   false,             // whether to compress or not
 54	})
 55	debugFileCore := zapcore.NewCore(encoder, os.Stdout, debugPriority)
 56	if logType == "file" {
 57		debugFileCore = zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(debugFileWriteSyncer, zapcore.AddSync(os.Stdout)), debugPriority)
 58	}
 59	// writeSyncer for info file
 60	infoFileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
 61		Filename:   "./log/info.log",
 62		MaxSize:    128,
 63		MaxBackups: 3,
 64		MaxAge:     10,
 65		Compress:   false,
 66	})
 67	infoFileCore := zapcore.NewCore(encoder, os.Stdout, infoPriority)
 68	if logType == "file" {
 69		infoFileCore = zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(infoFileWriteSyncer, zapcore.AddSync(os.Stdout)), infoPriority)
 70	}
 71	// writeSyncer for error file
 72	errorFileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
 73		Filename:   "./log/error.log",
 74		MaxSize:    128,
 75		MaxBackups: 5,
 76		MaxAge:     10,
 77		Compress:   false,
 78	})
 79	errorFileCore := zapcore.NewCore(encoder, os.Stdout, errorPriority)
 80	if logType == "file" {
 81		errorFileCore = zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(errorFileWriteSyncer, zapcore.AddSync(os.Stdout)), errorPriority)
 82	}
 83
 84	switch priority {
 85	case "":
 86		coreArr = append(coreArr, infoFileCore)
 87		coreArr = append(coreArr, errorFileCore)
 88	case "info":
 89		coreArr = append(coreArr, infoFileCore)
 90		coreArr = append(coreArr, errorFileCore)
 91	case "error":
 92		coreArr = append(coreArr, errorFileCore)
 93	case "debug":
 94		coreArr = append(coreArr, debugFileCore)
 95		coreArr = append(coreArr, infoFileCore)
 96		coreArr = append(coreArr, errorFileCore)
 97	}
 98	logger = zap.New(zapcore.NewTee(coreArr...), zap.AddCaller()) //zap.AddCaller() is to show the line number
 99	return logger
100}

4. 效果演示

 1package main
 2
 3import (
 4	"xxx/pkg/logger"
 5)
 6
 7// var log = logger.DefaultLog
 8var log = logger.InitLogger("", "", "debug").Sugar()
 9
10func main() {
11	log.Debug("debug")
12	log.Info("info")
13	log.Warn("warn")
14	log.Error("error")
15	log.Fatal("fatal")
16	log.Panic("panic")
17}
  • 输出

在性能开销很小的情况下,还可以清晰的展示日志级别,并且使用颜色区分, 十分强大美观,欢迎读者拷贝并使用我个人这份配置,有疑问我们下面评论区随时沟通探讨。