go zap自定义日志输出格式

一、背景

后端开发过程中,经常会涉及到日志框架的选取问题,对于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}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容