一、前言 在每个项目中,日志框架是必不可少的,它可以为我们打印关键日志,方便我们后续排查问题。一个好的日志框架可以提供以下功能:
日志可输出到控制台和文件
可根据文件大小或日期来切割日志文件,支持配置日志保留时间等
有日志调用的文件及行号、打印时间等
有日志输出级别:DEBUG、INFO、ERROR等
在 Go 项目中,有很多日志库,比较流行的有 golang 自带的 log 库、sirupsen 开源的 logrus 库、还有 uber 开源的 zap 库。在这里我们选择使用 zap 库,都说这个日志库的性能高,第二就是对 zap 也是颇有眼缘。
二、默认的Go Logger 在介绍 Uber 开源的 zap 库之前,我们先来讲解一下 Go 标准库自带的 log 库。也是有用处的,比如在项目【初始化配置文件】模块中,由于那时候还没有加载 zap 日志框架,所以就先调用 log 库来将【初始化配置文件】模块的相关日志输出到文件和控制台。
1、初始化 log 实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var ( logger *log.Logger ) const ( logDir = "logs" logFile = "xxx.log" logPath = logDir + "/" + logFile ) func init () { _ = os.Mkdir(logDir, os.ModeDir|0744 ) file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644 ) if err != nil { log.Fatalln("Failed to open log file: " , err) } logger = log.New(io.MultiWriter(os.Stderr, file), "" , log.LstdFlags|log.Lshortfile) }
以上代码就初始化了 log.Logger 实例,并实现将日志打印到文件和控制台。
上述初始化打印出来的日志样式是这样的:
1 2022/01/28 10:54:24 config.go:60: this is a log.
2、用法 共三种类型,分别是 Fatal、Print、Panic:
1 2 3 logger.Fatal("..." ) logger.Fatalf("..." ) logger.Fatalln("..." ) logger.Print("..." ) logger.Printf("..." ) logger.Println("..." ) logger.Panic("..." ) logger.Panicf("..." ) logger.Panicln("..." )
通过看源码就可以知道,这三种类型的实现都和 fmt.Sprint() 相关函数有关系,由于涉及了 interface{} 接口,所以性能有所损耗。并且也不支持日志级别。
Fatal 代表:日志打印,并且退出程序
Print 代表:普通的日志打印
Panic 代表:日志打印,并且恐慌异常
三、Uber 开源的 Zap 库 如果不想看 zap 日志库初始化的讲解,可直接下划到底部,获取 zap 日志初始化的全部代码。
1、配置日志编码器 首先配置日志编码器,包括日志记录字段、日志时间格式等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var encoderConfig = zapcore.EncoderConfig{ TimeKey: "time" , LevelKey: "level" , NameKey: "logger" , CallerKey: "linenum" , MessageKey: "msg" , StacktraceKey: "stacktrace" , LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeTime: func (t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format("2006-01-02 15:04:05" )) }, EncodeCaller: zapcore.ShortCallerEncoder, EncodeName: zapcore.FullNameEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, }
效果会变成这种,比较常用,推荐:
1 2022-01-28 10:56:47 ERROR this is a log.
2、设置日志级别 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 level := global.Config.GetString("server.log.level" ) fmt.Println("日志配置: " , level) logLevel := zap.DebugLevel switch level {case "debug" : logLevel = zap.DebugLevel case "info" : logLevel = zap.InfoLevel case "warn" : logLevel = zap.WarnLevel case "error" : logLevel = zap.ErrorLevel case "dpanic" : logLevel = zap.DPanicLevel case "panic" : logLevel = zap.PanicLevel case "fatal" : logLevel = zap.FatalLevel default : logLevel = zap.InfoLevel } atomicLevel := zap.NewAtomicLevel() atomicLevel.SetLevel(logLevel)
3、日志文件分割 1 2 3 4 5 6 7 8 9 writeSyncer := lumberjack.Logger{ Filename: logPath, MaxSize: global.Config.GetInt("server.log.maxSize" ), MaxBackups: global.Config.GetInt("server.log.maxBackups" ), MaxAge: global.Config.GetInt("server.log.maxAge" ), Compress: global.Config.GetBool("server.log.compress" ), LocalTime: true , }
4、打印文件行号 1 2 caller := zap.AddCaller()
5、初始化 Logger 实例 1 2 3 4 5 6 7 8 9 core := zapcore.NewCore( zapcore.NewConsoleEncoder(encoderConfig), zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&writeSyncer)), atomicLevel, ) log := zap.New(core, caller)
至此,zap logger 实例初始化完成,不过用法与寻常 log 不同,请看原因及用法:
由于fmt.Printf
之类的方法大量使用interface{}
和反射,会有不少性能损失,并且增加了内存分配的频次。zap
为了提高性能、减少内存分配次数,没有使用反射,而且默认的Logger
只支持强类型的、结构化的日志。必须使用zap
提供的方法记录字段。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
:二进制串的字段。
我写一个例子:
1 2 3 4 log.Info("this is a log" , zap.String("name" , "zhang3" ), zap.Int("age" , 26 ), zap.Duration("time" , 5 * time.Second))
效果为:
1 2022 -01 -28 23 :07 :36 INFO initialize/logger.go :107 this is a log {"name" : "zhang3" , "age" : 26 , "time" : 5 }
不太好用,每个字段都用方法包一层用起来比较繁琐。zap 也提供了便捷的方法 SugarLogger,可以使用 printf 格式符的方式。调用 logger.Sugar() 即可创建 SugaredLogger。SugaredLogger 的使用比 Logger 简单,只是性能比 Logger 低 50% 左右,可以用在非热点函数中。
1 2 sugarLog = log.Sugar() sugarLog.Info("Logger is OK." )
效果为:
1 2022 -01 -28 23 :07 :36 INFO initialize/logger.go :112 Logger is OK.
6、赋值到全局变量 创建 global 目录,存放全局变量
1 2 3 4 5 6 7 8 9 package globalimport ( "go.uber.org/zap" ) var ( Logger *zap.SugaredLogger )
在步骤 5 后面追加代码:
1 2 global.Logger = sugarLog global.Logger.Info("Logger is OK." )
7、用法 赋值给全局变量以后,在项目的任何地方都可以直接调用 global.Logger 来打印日志了,用法如下:
1 2 3 global.Logger.Info("..." ) global.Logger.Debug("..." ) global.Logger.Error("..." )
还有很多用法,也支持 Printf 模板变量的形式。
四、整体 logger 部分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 package initializeimport ( "fmt" "github.com/natefinch/lumberjack" "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" "xxx/global" "time" ) const ( logDir = "logs" logFile = "xxx.log" logPath = logDir + "/" + logFile ) func InitLogger () { var encoderConfig = zapcore.EncoderConfig{ TimeKey: "time" , LevelKey: "level" , NameKey: "logger" , CallerKey: "linenum" , MessageKey: "msg" , StacktraceKey: "stacktrace" , LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeTime: func (t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format("2006-01-02 15:04:05" )) }, EncodeCaller: zapcore.ShortCallerEncoder, EncodeName: zapcore.FullNameEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, } writeSyncer := lumberjack.Logger{ Filename: logPath, MaxSize: global.Config.GetInt("server.log.maxSize" ), MaxBackups: global.Config.GetInt("server.log.maxBackups" ), MaxAge: global.Config.GetInt("server.log.maxAge" ), Compress: global.Config.GetBool("server.log.compress" ), LocalTime: true , } level := global.Config.GetString("server.log.level" ) fmt.Println("日志配置: " , level) logLevel := zap.DebugLevel switch level { case "debug" : logLevel = zap.DebugLevel case "info" : logLevel = zap.InfoLevel case "warn" : logLevel = zap.WarnLevel case "error" : logLevel = zap.ErrorLevel case "dpanic" : logLevel = zap.DPanicLevel case "panic" : logLevel = zap.PanicLevel case "fatal" : logLevel = zap.FatalLevel default : logLevel = zap.InfoLevel } atomicLevel := zap.NewAtomicLevel() atomicLevel.SetLevel(logLevel) core := zapcore.NewCore( zapcore.NewConsoleEncoder(encoderConfig), zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&writeSyncer)), atomicLevel, ) caller := zap.AddCaller() log := zap.New(core, caller) global.Logger = log.Sugar() global.Logger.Info("Logger is OK." ) }
在 main.go 里面,调用 InitLogger() 即可。另外,zap 底层 API 可以设置缓存,所以一般使用 defer logger.Sync() 将缓存同步到文件中。defer 的作用大家都懂吧,延迟函数。
1 2 3 4 5 func main () { initialize.InitLogger() defer global.Logger.Sync() }
五、总结 本文先讲解了 Go 标准库自带的 log 库的初始化方法,实现了日志打印到文件和控制台。但由于自带的 log 库没有日志级别,也不支持日志切割,所以又根据性能比较选择了 Uber 开源的 zap 日志库。
zap 日志库实现了:
日志可输出到控制台和文件
可根据文件大小或日期来切割日志文件,支持配置日志保留时间等
有日志调用的文件及行号、打印时间等
有日志输出级别:DEBUG、INFO、ERROR等
但 zap 的 Logger 为了提高性能,只支持强类型的、结构化的日志,相对来说不是很好用。但如果对于热点函数,频繁调用日志,zap 默认的 Logger 仍是首选。为了简化调用,zap 就推出了 SugarLogger。SugarLogger 可以使用 printf 格式符的方式。调用 log.Sugar() 即可创建 SugaredLogger 。SugaredLogger 的使用比 Logger 简单,只是性能比 Logger 低 50% 左右,不过我项目中依旧使用了 SugarLogger 。