Gorm慢查詢、SQL日志與Go項(xiàng)目日志的整合與串聯(lián)
如果僅集成到這個(gè)程度,功能開發(fā)完全沒有問題,但如果你還要長(zhǎng)期維護(hù)項(xiàng)目的話,那么問題可大了去了。
原因是,GORM產(chǎn)生的SQL記錄、慢查詢、以及數(shù)據(jù)庫錯(cuò)誤都是通過GORM自身的GormLogger寫到日志的,而且GormLogger默認(rèn)是寫到標(biāo)準(zhǔn)輸出的。。。而不是項(xiàng)目本身的日志文件里,這樣一來如果真的是SQL錯(cuò)誤、慢查詢等導(dǎo)致的Bug,你只能知道發(fā)生Error了,但是在項(xiàng)目日志里卻找不到Error的明細(xì),那你解決BUG就只能靠猜和試?yán)病?/p>
圖片
所以這節(jié)內(nèi)容我們介紹怎么把GORM日志和項(xiàng)目日志整合到一起,讓你擁有下面這樣的SQL詳細(xì)信息
{
"level": "debug",
"ts": "2024-10-09T17:09:07.603+0800",
"msg": "SQL DEBUG",
"sql": "INSERT INTO `orders` (`user_id`,`bill_money`,`order_no`,`state`,`is_del`,`created_at`,`updated_at`) VALUES (123453453,20,'20240627596615375920904456',1,0,'2024-10-09 17:09:07.586','2024-10-09 17:09:07.586')",
"rows": 1,
"dur(ms)": 53,
"traceid": "19d822280c64c5ed",
"spanid": "450ccc402492ed45",
"pspanid": "",
"file": "gormlog.go",
"line": 49
}
支持慢查詢、SQL錯(cuò)誤的詳細(xì)記錄,在開發(fā)環(huán)境還會(huì)增加SQL DEBUG輸出把執(zhí)行的SQL語句輸出到日志文件和控制臺(tái),方便在開發(fā)階段進(jìn)行調(diào)試和驗(yàn)證。
我們同樣會(huì)為GORM日志注入追蹤ID,把它們歸因到關(guān)聯(lián)的請(qǐng)求上下文中去。這樣一旦發(fā)生數(shù)據(jù)庫相關(guān)的錯(cuò)誤,我們可以通過監(jiān)控主動(dòng)發(fā)現(xiàn)、使用錯(cuò)誤日志中的追蹤ID還能把整個(gè)請(qǐng)求相關(guān)的日志都檢索出來,查問題也會(huì)輕松很多。
加入項(xiàng)目后訪問https://github.com/go-study-lab/go-mall/compare/c6...c7 能查看我們?cè)陧?xiàng)目中定制化GORM的整個(gè)過程
圖片
Gorm Logger Interface
GORM 允許我們自定義Logger把它執(zhí)行的數(shù)據(jù)庫記錄、產(chǎn)生的錯(cuò)誤等都記錄到項(xiàng)目自身的日志文件中去,
V1版本的GROM的 logger 接口長(zhǎng)這個(gè)樣子,僅僅提供了一個(gè)Print方法,Print方法的參數(shù)都是 create、updates、query 這些方法的回調(diào)中傳遞過去的,我們并沒有辦法傳遞Context。
type logger interface {
Print(v ...interface{})
}
所以前幾年的項(xiàng)目流行使用GLS -- Goroutine Local Storage,因?yàn)楣俜讲煌扑]也不認(rèn)可GLS這個(gè)概念,GLS的實(shí)現(xiàn)都是第三方庫,前幾年我之前公司項(xiàng)目自己封裝的Logger里就大量使用 jtolio/gls 這個(gè)庫實(shí)現(xiàn)的GLS。
后來做新項(xiàng)目時(shí)我曾經(jīng)還想偷懶直接使用之前公司的Logger,結(jié)果發(fā)現(xiàn)Go 1.19 版本以上 jtolio/gls 這個(gè)庫就不能用了。
jtolio/gls 倉(cāng)庫地址: https://github.com/jtolio/gls
好在GORM在V2 版本的Logger 中增加了Context 回調(diào)的能力,這樣就能把執(zhí)行SQL時(shí)的上下文信息也記錄到日志中來,這點(diǎn)在V1 版本是做不到的。
在GORM V2 中它新增了以下Logger 接口 logger.Interface:
type Interface interface {
LogMode(LogLevel) Interface
Info(context.Context, string, ...interface{})
Warn(context.Context, string, ...interface{})
Error(context.Context, string, ...interface{})
Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
}
每個(gè)方法都有應(yīng)用的上下文Context參數(shù)傳遞進(jìn)來,還專門提供了Trace方法讓我們實(shí)現(xiàn),供我們實(shí)現(xiàn)查詢的SQL語句和消耗時(shí)間的記錄。
接下來我們開始自定義GORM Logger,其中使用上我們前面已經(jīng)封裝好的項(xiàng)目的日志門面,通過日志門面實(shí)現(xiàn)把GORM日志和項(xiàng)目日志記錄到同一個(gè)地方。
實(shí)現(xiàn)GormLogger 把GORM日志整合到項(xiàng)目日志
現(xiàn)在我們?cè)陧?xiàng)目的 dal/dao 目錄下新建gormlog.go 文件,并添加以下代碼。
type GormLogger struct {
SlowThreshold time.Duration
}
func NewGormLogger() *GormLogger {
return &GormLogger{
SlowThreshold: 500 * time.Millisecond,
}
新建GormLogger類型,在類型中的字段 SlowThreshold 用于設(shè)置慢查詢的門檻,我們?cè)谶@里設(shè)置超過500ms 就是慢查詢,如果需要按環(huán)境定制化, 就把這個(gè)做成配置項(xiàng)放到配置文件中去。
接下來用GormLogger 實(shí)現(xiàn)上面GORM的logger.Interface 中定義的所有方法:
func (l *GormLogger) LogMode(lev gormLogger.LogLevel) gormLogger.Interface {
return &GormLogger{}
}
func (l *GormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
logger.New(ctx).Info(msg, "data", data)
}
func (l *GormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
logger.New(ctx).Error(msg, "data", data)
}
func (l *GormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
logger.New(ctx).Error(msg, "data", data)
}
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
// 獲取運(yùn)行時(shí)間
duration := time.Since(begin).Milliseconds()
// 獲取 SQL 語句和返回條數(shù)
sql, rows := fc()
// Gorm 錯(cuò)誤時(shí)記錄錯(cuò)誤日志
if err != nil {
logger.New(ctx).Error("SQL ERROR", "sql", sql, "rows", rows, "dur(ms)", duration)
}
// 慢查詢?nèi)罩? if duration > l.SlowThreshold.Milliseconds() {
logger.New(ctx).Warn("SQL SLOW", "sql", sql, "rows", rows, "dur(ms)", duration)
} else {
logger.New(ctx).Debug("SQL DEBUG", "sql", sql, "rows", rows, "dur(ms)", duration)
}
}
注意因?yàn)镚ORM的logger包跟我們自己的日志門面 logger 包名稱沖突了,所以在導(dǎo)入包的時(shí)候要給它設(shè)置gormLogger這個(gè)別名。
上面的實(shí)現(xiàn)主要的邏輯是對(duì)Trace方法的重寫,GORM在執(zhí)行SQL回調(diào)Trace方法時(shí),會(huì)把執(zhí)行開始的時(shí)間、執(zhí)行的SQL、返回的函數(shù)以及執(zhí)行中的錯(cuò)誤告訴我們。如果執(zhí)行中發(fā)生錯(cuò)誤就記錄錯(cuò)誤日志,如果消耗時(shí)間超過我們約定的500ms就記一條Warn級(jí)別的日志。