一、背景
后端开发过程中,经常会涉及到日志框架的选取问题,对于go项目,类似框架也很多,eg:zap、zerolog、logrus等。由于读写日志都是一个比较频繁的操作,因此性能是我们首先考虑的问题。在此我选取的是zap日志包。zap由uber开源,由于其快速、结构化、高性能等优点,而受到众程序员的热爱,我们来看几张由Zap官方提供的基准测试对照表:
从中我们可以看出zerolog是与Zap竞争最激烈的。zerolo还提供结果非常相似的基准测试。
二、自定义zap日志
我们来看下zap日志包的工作流程图:
基于此,我整理以下自定义zap日志常规思路:
package main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
)
var logger *zap.Logger
func main() {
// 6 初始化logger
GetLogger()
// 7 打印日志
logger.Info("自定义logger", zap.String("name", "zap log"))
logger.Debug("自定义logger", zap.String("name", "zap log"))
}
func GetLogger() {
// 1 用new自定义log日志
// zap.New(xxx)
// 2 zap.New需要接收一个core,core是zapcore.Core类型,zapcore.Core是一个interface类型,
// 而zapcore.NewCore返回的ioCore刚好实现了这个接口类型的所有5个方法,那么NewCore也可以认为是core类型
// 3 所以zap.New(core)变成了zap.New(zapcore.NewCore)
// 4 而zapcore.NewCore需要三个变量:Encoder, WriteSyncer, LevelEnabler,我们在创建NewCore时自定义这三个类型变量即可,其中:
// Encoder:编码器 (写入日志格式)
// WriteSyncer:指定日志写到哪里去
// LevelEnabler:日志打印级别
// NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler)
// 4.2 通过GetEncoder获取自定义的Encoder
Encoder := GetEncoder()
// 4.4 通过GetWriteSyncer获取自定义的WriteSyncer
WriteSyncer := GetWriteSyncer()
// 4.6 通过GetLevelEnabler获取自定义的LevelEnabler
LevelEnabler := GetLevelEnabler()
// 4.7 通过Encoder、WriteSyncer、LevelEnabler创建一个core
newCore := zapcore.NewCore(Encoder, WriteSyncer, LevelEnabler)
// 5 传递 newCore New一个logger
// zap.AddCaller(): 输出文件名和行号
// zap.Fields: 假如每条日志中需要携带公用的信息,可以在这里进行添加
logger = zap.New(newCore)
}
// GetEncoder 自定义的Encoder 4.1
// 打开zapcore的源码,见图“zapcore-Encoder”:发现其中有两个new Encoder的func:
// NewConsoleEncoder(console_encoder.go)
// NewJSONEncoder(json_encoder.go)
// 这两个func都需要传递一个EncoderConfig的变量,而zap中已经给我们提供了几种获取EncoderConfig的方式
// zap.NewProductionEncoderConfig()
// zap.NewDevelopmentEncoderConfig()
// 在这里我直接把zap.NewProductionEncoderConfig()源码中的部分黏贴过来
func GetEncoder() zapcore.Encoder {
return zapcore.NewConsoleEncoder(
zapcore.EncoderConfig{
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding, // 默认换行符"\n"
EncodeLevel: zapcore.LowercaseLevelEncoder, // 日志等级序列为小写字符串,如:InfoLevel被序列化为 "info"
EncodeTime: zapcore.EpochTimeEncoder, // 日志时间格式显示
EncodeDuration: zapcore.SecondsDurationEncoder, // 时间序列化,Duration为经过的浮点秒数
EncodeCaller: zapcore.ShortCallerEncoder, // 日志行号显示
})
}
// GetWriteSyncer 自定义的WriteSyncer 4.3
func GetWriteSyncer() zapcore.WriteSyncer {
file, _ := os.Create("./zap.log")
return zapcore.AddSync(file)
}
// GetLevelEnabler 自定义的LevelEnabler 4.5
func GetLevelEnabler() zapcore.Level {
return zapcore.InfoLevel // 只会打印出info及其以上级别的日志
}
# 上述脚本输出的日志格式如下:
1.6475221235018082e+09 info 自定义logger {"name": "zap log"}
三、扩展、完善
对于上面“二”其实还有很多扩展和完善的地方,如下:
3.1、我们通过上面方式定义的logger,每次打印日志的时候,需要这样操作:logger.Info、logger.Debug等,如果zap内有全局的logger,然后我们将自己定义的logger替换成全局的logger,此后打印日志的时候,全局logger是否有一种更为简便的方式。
3.2、如果将文件同时输出到控制台和文件
3.3、文件过大如何切分
3.4、如下格式的日志,如何自定义:
[2022-03-17 21:32:24] [INFO] [log_demo/main.go:21] 自定义logger
3.5、在gin框架中如何载入自定义的日志
对于上面的问题,逐一解答:
3.1、为了方便使用,zap提供了两个全局的Logger,一个是*zap.Logger,可调用zap.L()获得;另一个是*zap.SugaredLogger,可调用zap.S()获得。需要注意的是,全局的Logger默认并不会记录日志!它是一个无实际效果的Logger。看下源码:
// [go.uber.org/zap/global.go](http://go.uber.org/zap/global.go)
var (
_globalMu sync.RWMutex
_globalL = NewNop()
_globalS = _globalL.Sugar()
)
3.2、使用zapcore.NewTee
3.3、使用github.com/natefinch/lumberjack包
3.4、分析zapcore.LowercaseLevelEncoder、zapcore.EpochTimeEncoder、zapcore.ShortCallerEncoder进行充血
3.5、gin中使用上面自定义的logger见下代码
最终代码见下:
package main
import (
"github.com/gin-gonic/gin"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net/http"
"os"
"time"
)
const (
logTmFmt = "2006-01-02 15:04:05"
)
func main() {
GetLogger()
r := gin.New()
r.Use(GinLogger())
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"version": "v1.1",
})
})
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "404",
})
})
r.Run(":9090")
}
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
zap.L().Info(path,
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
func GetLogger() {
Encoder := GetEncoder()
WriteSyncer := GetWriteSyncer()
LevelEnabler := GetLevelEnabler()
ConsoleEncoder := GetConsoleEncoder()
newCore := zapcore.NewTee(
zapcore.NewCore(Encoder, WriteSyncer, LevelEnabler), // 写入文件
zapcore.NewCore(ConsoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel), // 写入控制台
)
logger := zap.New(newCore, zap.AddCaller())
zap.ReplaceGlobals(logger)
}
// GetEncoder 自定义的Encoder
func GetEncoder() zapcore.Encoder {
return zapcore.NewConsoleEncoder(
zapcore.EncoderConfig{
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller_line",
FunctionKey: zapcore.OmitKey,
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: " ",
EncodeLevel: cEncodeLevel,
EncodeTime: cEncodeTime,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: cEncodeCaller,
})
}
// GetConsoleEncoder 输出日志到控制台
func GetConsoleEncoder() zapcore.Encoder {
return zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
}
// GetWriteSyncer 自定义的WriteSyncer
func GetWriteSyncer() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: "./zap.log",
MaxSize: 200,
MaxBackups: 10,
MaxAge: 30,
}
return zapcore.AddSync(lumberJackLogger)
}
// GetLevelEnabler 自定义的LevelEnabler
func GetLevelEnabler() zapcore.Level {
return zapcore.InfoLevel
}
// cEncodeLevel 自定义日志级别显示
func cEncodeLevel(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString("[" + level.CapitalString() + "]")
}
// cEncodeTime 自定义时间格式显示
func cEncodeTime(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString("[" + t.Format(logTmFmt) + "]")
}
// cEncodeCaller 自定义行号显示
func cEncodeCaller(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString("[" + caller.TrimmedPath() + "]")
}
日志格式见下(控制台和文件中都会输出):
[2022-03-17 22:31:59] [INFO] [log_demo/main.go:41] / {"status": 200, "method": "GET", "path": "/", "query": "", "ip": "127.0.0.1", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36", "errors": "", "cost": 0.000111808}