# 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`