一、前言

在每个项目中,日志框架是必不可少的,它可以为我们打印关键日志,方便我们后续排查问题。一个好的日志框架可以提供以下功能:

  • 日志可输出到控制台和文件
  • 可根据文件大小或日期来切割日志文件,支持配置日志保留时间等
  • 有日志调用的文件及行号、打印时间等
  • 有日志输出级别: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
)

//
// @Description: 此时还没有初始化日志配置,所以就用了go自带的log库来打印日志到控制台和文件
// go log 初始化,参考自:https://www.flysnow.org/2017/05/06/go-in-action-go-log.html
//
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,
// ISO8601 UTC 时间格式
//EncodeTime: zapcore.ISO8601TimeEncoder,
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,
//EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
// enc.AppendInt64(int64(d) / 1000000)
//},
}

效果会变成这种,比较常用,推荐:

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
// 获取 info、error日志文件的io.Writer 抽象 getWriter() 在下方实现
writeSyncer := lumberjack.Logger{
Filename: logPath, // 日志文件路径
MaxSize: global.Config.GetInt("server.log.maxSize"), //文件大小限制,单位MB
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.TypeTypebool/int/uint/float64/complex64/time.Time/time.Duration/error等)就表示该类型的字段,zap.Typepp结尾表示该类型指针的字段,zap.Typess结尾表示该类型切片的字段。如:

  • zap.Bool(key string, val bool) Fieldbool字段
  • zap.Boolp(key string, val *bool) Fieldbool指针字段;
  • zap.Bools(key string, val []bool) Fieldbool切片字段。

当然也有一些特殊类型的字段:

  • 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 global

import (
"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 initialize

import (
"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
)

//
// @Description: 初始化zap日志配置
//
func InitLogger() {
// 日志编码器配置,包括日志记录字段、日志时间格式等
var encoderConfig = zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "linenum",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
// ISO8601 UTC 时间格式
//EncodeTime: zapcore.ISO8601TimeEncoder,
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,
//EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
// enc.AppendInt64(int64(d) / 1000000)
//},
}
//// 实现判断日志等级的interface
//infoLevel := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { //info级别
// return lev >= zap.InfoLevel
//})
//errorLevel := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
// return lev >= zapcore.ErrorLevel
//})
// 通过lumberjack实现日志分割
writeSyncer := lumberjack.Logger{
Filename: logPath, // 日志文件路径
MaxSize: global.Config.GetInt("server.log.maxSize"), //文件大小限制,单位MB
MaxBackups: global.Config.GetInt("server.log.maxBackups"), //最大保留日志文件数量
MaxAge: global.Config.GetInt("server.log.maxAge"), //日志文件保留天数
Compress: global.Config.GetBool("server.log.compress"), //是否压缩
LocalTime: true,
}
//errorWriter := getWriter("./logs/error.log")

// 日志级别配置
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)
// 最后创建具体的Logger
core := zapcore.NewCore(
// 编码器配置
zapcore.NewConsoleEncoder(encoderConfig),
// 打印到控制台和文件
zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&writeSyncer)),
// 日志级别
atomicLevel,
)

//core := zapcore.NewTee(
// // 控制台输出
// zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), infoLevel),
// // 文件输出
// zapcore.NewCore(encoder, zapcore.AddSync(infoWriter), infoLevel),
// //zapcore.NewCore(encoder, zapcore.AddSync(errorWriter), errorLevel),
//)
// 打印文件名及行号
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 。