go zap自定义日志输出格式

2022-03-17  本文已影响0人  fengzhihai

一、背景

后端开发过程中,经常会涉及到日志框架的选取问题,对于go项目,类似框架也很多,eg:zap、zerolog、logrus等。由于读写日志都是一个比较频繁的操作,因此性能是我们首先考虑的问题。在此我选取的是zap日志包。zap由uber开源,由于其快速、结构化、高性能等优点,而受到众程序员的热爱,我们来看几张由Zap官方提供的基准测试对照表:


日志性能对照表1
日志性能对照表2
日志性能对照表3

从中我们可以看出zerolog是与Zap竞争最激烈的。zerolo还提供结果非常相似的基准测试。

二、自定义zap日志

我们来看下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及其以上级别的日志
}
zapcore-Encoder
# 上述脚本输出的日志格式如下:
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}
上一篇 下一篇

猜你喜欢

热点阅读