go zap自定義日志輸出格式

一箍铭、背景

后端開發(fā)過程中,經(jīng)常會(huì)涉及到日志框架的選取問題椎镣,對(duì)于go項(xiàng)目诈火,類似框架也很多,eg:zap状答、zerolog冷守、logrus等。由于讀寫日志都是一個(gè)比較頻繁的操作惊科,因此性能是我們首先考慮的問題拍摇。在此我選取的是zap日志包。zap由uber開源馆截,由于其快速充活、結(jié)構(gòu)化蜂莉、高性能等優(yōu)點(diǎn),而受到眾程序員的熱愛混卵,我們來看幾張由Zap官方提供的基準(zhǔn)測試對(duì)照表:


日志性能對(duì)照表1

日志性能對(duì)照表2

日志性能對(duì)照表3

從中我們可以看出zerolog是與Zap競爭最激烈的映穗。zerolo還提供結(jié)果非常相似的基準(zhǔn)測試。

二幕随、自定義zap日志

我們來看下zap日志包的工作流程圖:


zap日志包工作流程圖

基于此蚁滋,我整理以下自定義zap日志常規(guī)思路:

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需要接收一個(gè)core,core是zapcore.Core類型赘淮,zapcore.Core是一個(gè)interface類型辕录,
    //   而zapcore.NewCore返回的ioCore剛好實(shí)現(xiàn)了這個(gè)接口類型的所有5個(gè)方法,那么NewCore也可以認(rèn)為是core類型
    // 3 所以zap.New(core)變成了zap.New(zapcore.NewCore)
    // 4 而zapcore.NewCore需要三個(gè)變量:Encoder, WriteSyncer, LevelEnabler,我們在創(chuàng)建NewCore時(shí)自定義這三個(gè)類型變量即可梢卸,其中:
    //         Encoder:編碼器 (寫入日志格式)
    //         WriteSyncer:指定日志寫到哪里去
    //         LevelEnabler:日志打印級(jí)別
    // 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創(chuàng)建一個(gè)core
    newCore := zapcore.NewCore(Encoder, WriteSyncer, LevelEnabler)
    // 5 傳遞 newCore New一個(gè)logger
    //  zap.AddCaller(): 輸出文件名和行號(hào)
    //  zap.Fields: 假如每條日志中需要攜帶公用的信息低剔,可以在這里進(jìn)行添加
    logger = zap.New(newCore)
}

// GetEncoder 自定義的Encoder    4.1
//    打開zapcore的源碼速梗,見圖“zapcore-Encoder”:發(fā)現(xiàn)其中有兩個(gè)new Encoder的func:
//          NewConsoleEncoder(console_encoder.go)
//          NewJSONEncoder(json_encoder.go)
//    這兩個(gè)func都需要傳遞一個(gè)EncoderConfig的變量,而zap中已經(jīng)給我們提供了幾種獲取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,      // 默認(rèn)換行符"\n"
            EncodeLevel:    zapcore.LowercaseLevelEncoder,  // 日志等級(jí)序列為小寫字符串襟齿,如:InfoLevel被序列化為 "info"
            EncodeTime:     zapcore.EpochTimeEncoder,       // 日志時(shí)間格式顯示
            EncodeDuration: zapcore.SecondsDurationEncoder, // 時(shí)間序列化姻锁,Duration為經(jīng)過的浮點(diǎn)秒數(shù)
            EncodeCaller:   zapcore.ShortCallerEncoder,     // 日志行號(hào)顯示
        })
}

// 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 // 只會(huì)打印出info及其以上級(jí)別的日志
}
zapcore-Encoder
# 上述腳本輸出的日志格式如下:
1.6475221235018082e+09  info    自定義logger   {"name": "zap log"}

三、擴(kuò)展猜欺、完善

對(duì)于上面“二”其實(shí)還有很多擴(kuò)展和完善的地方位隶,如下:

3.1、我們通過上面方式定義的logger开皿,每次打印日志的時(shí)候涧黄,需要這樣操作:logger.Info、logger.Debug等赋荆,如果zap內(nèi)有全局的logger笋妥,然后我們將自己定義的logger替換成全局的logger,此后打印日志的時(shí)候窄潭,全局logger是否有一種更為簡便的方式春宣。
3.2、如果將文件同時(shí)輸出到控制臺(tái)和文件
3.3嫉你、文件過大如何切分
3.4月帝、如下格式的日志,如何自定義:
[2022-03-17 21:32:24]   [INFO]  [log_demo/main.go:21]   自定義logger
3.5幽污、在gin框架中如何載入自定義的日志

對(duì)于上面的問題嚷辅,逐一解答:

3.1、為了方便使用距误,zap提供了兩個(gè)全局的Logger簸搞,一個(gè)是*zap.Logger扁位,可調(diào)用zap.L()獲得;另一個(gè)是*zap.SugaredLogger趁俊,可調(diào)用zap.S()獲得贤牛。需要注意的是,全局的Logger默認(rèn)并不會(huì)記錄日志则酝!它是一個(gè)無實(shí)際效果的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進(jìn)行充血
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), // 寫入控制臺(tái)
    )
    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 輸出日志到控制臺(tái)
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 自定義日志級(jí)別顯示
func cEncodeLevel(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
    enc.AppendString("[" + level.CapitalString() + "]")
}

// cEncodeTime 自定義時(shí)間格式顯示
func cEncodeTime(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    enc.AppendString("[" + t.Format(logTmFmt) + "]")
}

// cEncodeCaller 自定義行號(hào)顯示
func cEncodeCaller(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
    enc.AppendString("[" + caller.TrimmedPath() + "]")
}
日志格式見下(控制臺(tái)和文件中都會(huì)輸出):
[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}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末挚瘟,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子饲梭,更是在濱河造成了極大的恐慌乘盖,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件憔涉,死亡現(xiàn)場離奇詭異订框,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)兜叨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門穿扳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人国旷,你說我怎么就攤上這事矛物。” “怎么了跪但?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵履羞,是天一觀的道長。 經(jīng)常有香客問我特漩,道長吧雹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任涂身,我火速辦了婚禮雄卷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蛤售。我一直安慰自己丁鹉,他們只是感情好妒潭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揣钦,像睡著了一般雳灾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上冯凹,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天谎亩,我揣著相機(jī)與錄音,去河邊找鬼宇姚。 笑死匈庭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浑劳。 我是一名探鬼主播阱持,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼魔熏!你這毒婦竟也來了衷咽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤蒜绽,失蹤者是張志新(化名)和其女友劉穎镶骗,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體躲雅,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡卖词,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吏夯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片此蜈。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖噪生,靈堂內(nèi)的尸體忽然破棺而出裆赵,到底是詐尸還是另有隱情,我是刑警寧澤跺嗽,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布战授,位于F島的核電站,受9級(jí)特大地震影響桨嫁,放射性物質(zhì)發(fā)生泄漏植兰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一璃吧、第九天 我趴在偏房一處隱蔽的房頂上張望楣导。 院中可真熱鬧,春花似錦畜挨、人聲如沸筒繁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毡咏。三九已至驮宴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呕缭,已是汗流浹背堵泽。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恢总,地道東北人落恼。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像离熏,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子戴涝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容