# Zap logger 整合 Gin / Gorm 心得 來源是 gin-contrib 底下的 [zap](https://github.com/gin-contrib/zap) 專案,將 uber 貢獻的 [zap](https://github.com/uber-go/zap) log 工具整合進 gin,近期又有[熱心人士](https://github.com/OldSmokeGun)將專案優化成可以加入客製化的 log field,這個對我來說非常有吸引力,配合 [jq](https://medium.com/evan-fang/jq-命令列json處理工具-a553c8940ef5) 工具可以達到快速查找特定 log 的需求這篇筆記主要在紀錄目前使用後的心得 :::warning 由於這個 PR 的改動較大,貢獻者可能忘記修改相對的測試案例,所以這個 PR 目前還沒通過,因此直接 import lib 只會看到舊版的功能,因此我是直接 fork 到我的專案當作 lib 使用 ::: ## 使用方式 [Example](https://github.com/OldSmokeGun/zap/blob/master/_example/main.go) ```go= // Add a ginzap middleware, which: // - Logs all requests, like a combined access and error log. // - Logs to stdout. // - RFC3339 with UTC time format. r.Use(ginzap.Ginzap(logger, time.RFC3339, true)) ``` 如同註解,使用方式非常簡單,只要把 `zap.Logger` 餵進去 function 的第一個參數,再加上希望的時間格式以及是否為 utc 模式 >第三個參數若是傳進 `false`,就會以 local 時區顯示 這是作者建議的做法,我自己則是在稍微客製化一下 因為我想要將不同的 log 放在個別的資料夾,並且希望可以做到 rotation 首先我建了一個 CustomLogger, ```go= type CustomLogger struct { NormalLogger *zap.Logger DBLogger *zap.Logger ErrorLogger *zap.Logger } func (c *CustomLogger) Info(msg string, fields ...zap.Field) { c.NormalLogger.Info(msg, fields...) } func (c *CustomLogger) Debug(msg string, fields ...zap.Field) { c.NormalLogger.Debug(msg, fields...) } func (c *CustomLogger) Warn(msg string, fields ...zap.Field) { c.ErrorLogger.Warn(msg, fields...) } func (c *CustomLogger) Error(msg string, fields ...zap.Field) { c.ErrorLogger.Error(msg, fields...) } func (c *CustomLogger) Fatal(msg string, fields ...zap.Field) { c.ErrorLogger.Fatal(msg, fields...) } ``` 三種使用情境,將資料分別存放,並實作各種等級的 log function 可以看到我只是將一般/錯誤分別用不同 logger 去呼叫 `DBLogger` 是專門給 `gorm` 用的,每次 sql 執行命令都會紀錄一筆,所以我希望將它拆出來 接著我用了簡單的 [`Singleton`](https://skyyen999.gitbooks.io/-study-design-pattern-in-java/content/singleton.html) pattern 來處理這個 global logger ```go= var CLogger *CustomLogger var once sync.Once func GetLoggerInstance() *CustomLogger { once.Do(func() { CLogger = &CustomLogger{} CLogger.NormalLogger = newLogger(true, logFilePath) CLogger.DBLogger = newLogger(true, dbFilePath) CLogger.ErrorLogger = newLogger(true, errFilePath) }) return CLogger } ``` :::info golang 的 `sync` package 有很多好用的 lib,像這邊的 `once.Do` 可以確保這個動作只會執行一次,在處理 race condition 的情境也很好用 ::: 接下來就是把這個 logger 餵給 gin 的 `middleware` ```go= // log middleware r.Use(lib.Logger(util.GetLoggerInstance().NormalLogger, lib.WithTimeFormat(time.RFC3339Nano), // custom fields here // lib.WithCustomFields() )) // recover middleware r.Use(lib.Recovery(util.GetLoggerInstance().ErrorLogger, true, // custom fields here // lib.WithCustomFields() ) ``` 這邊可以看到我將 `CustomLogger` 的兩種 Logger 分別餵給 middleware ```go= type customLogger struct { logger *zap.Logger } func (c *customLogger) Write(p []byte) (n int, err error) { // parse // \d+.\d+[a-z]+ ---> latency // /.*:\d+ ---> file // [A-Z].*\n ---> syntax latency := regexp.MustCompile("\\d+.\\d+[a-z]+").Find(p) path := regexp.MustCompile(" /[A-Za-z].*:\\d+").Find(p) syntax := regexp.MustCompile(" [A-Z].*").Find(p) c.logger.Info("gorm sql log", zap.String("latency", strings.Trim(string(latency), " ")), zap.String("path", strings.Trim(string(path), " ")), zap.String("syntax", strings.Trim(string(syntax), " ")), ) return len(p), nil } ... // zap logger for gorm cl := customLogger{logger: util.GetLoggerInstance().DBLogger} // init gorm logger newLogger := logger.New( log.New(&cl, "", log.LstdFlags), // io writer logger.Config{ SlowThreshold: time.Second, // Slow SQL threshold LogLevel: logger.Info, // Log level IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger Colorful: false, // Disable color }, ) ``` 這邊是 DBLogger,看得出來比較麻煩,因為 gorm 的 logger 預設的 log 內容並沒有格式化,所以我**又**做了一個 `customLogger` 來接它傳過來的訊息,parse 之後再傳給 `DBLogger` 去寫入 --- 最後是一般使用場景 ```go= util.GetLoggerInstance().Debug( "bet detail", zap.String("label", "Ed"), zap.Any("details", details), zap.String("SeverID", ServerID), zap.String("NowRunNo", NowRunNo), zap.String("NowActiveNo", NowActiveNo), ) ``` 滿淺顯易懂的,在需要下 log 的地方用 `util.GetLoggerInstance()` 就可以拿到 `CustomLogger` 的實體並用它來寫 log,因為可以加入自定義的 field,而且是我想要的 json 格式,非常滿意,接下來會介紹搭配 jq 可以做到的效果 --- ## 利用 jq 查詢 [基本介紹](https://www.baeldung.com/linux/jq-command-json) jq 是支援 pipeline 的,所以使用上很方便,可以透過組合將想要的結果過濾出來,例如: ```shell= cat XXX.log | jq 'select(.label=="Ed")' ``` 可以過濾出特定的 label ```shell cat XXX.log | jq 'select(.msg | contains("hello world"))' ``` `contains` 搭配 `select` 的用法,很像 sql 中的 **WHERE LIKE** 語法,可以模糊比對目標的內容 這邊可以看到 jq 的 function 內部也支援 pipeline 的傳遞方式,將 select 出的 msg 拋給 contains 過濾 ```shell= cat XXX.log | jq 'select(.label=="Ed")' | jq 'select(.msg | contains("hello world"))' ``` 以上兩句搭配,就可以做到搜出特定 label 的並且內容包含 `hello world` 的log 利用這些特性,我們可以做出像是在查詢 sql 一般的效果,可以讓 debug 更有效率 ###### tags: `Golang`