记录sql的慢查询日志和错误日志是很有必要的。
gorm 版本 gorm.io/gorm v1.20.1
gorm提供了默认的logger实现:
if config.Logger == nil {
config.Logger = logger.Default
}
Default = New(log.New(os.Stdout, "\r\n", log.LstdFlags), Config{
SlowThreshold: 100 * time.Millisecond,
LogLevel: Warn,
Colorful: true,
})
可以看出默认logger的特点:
1、基于标准输出的。
2、慢sql的标准为 100毫秒。
3、彩色输出。
4、日期时间的格式为 2020/10/23 11:04:22 。
5、前缀为 \r\n 。
显然这种默认的logger是不能满足需求的,因此需要定义自己的Logger。
使用 gorm.io/gorm/logger
下的 New 方法来实例化一个 logger 。
func New(writer Writer, config Config) Interface
type Writer interface {
Printf(string, ...interface{})
}
需要一个实现了 Printf(string, ...interface{})
方法的对象。
正好golang提供的 log 包就可以作为一个 Writer,而且我的日志组件也是对 log 包的封装,
于是可以这样:
type Writer struct {
}
// 格式化外部组件日志
func (lw Writer) Printf(format string, v ...interface{}) {
Logger.SetPrefix("")
setLogFile()
Logger.Printf(format, v...)
}
newLogger := logger.New(
logging.LWriter,
logger.Config{
SlowThreshold: 1 * time.Second,
LogLevel: logger.Warn,
Colorful: false,
},
)
创建连接
gormDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true, // 禁用默认事务
Logger: newLogger,
PrepareStmt: true, // 创建并缓存预编译语句
})
这样 gorm 所有的日志就都会以日志的形式记录到日志文件。
需要注意的是 gorm 的日志只是使用了日志组件的资源,即:
logging.Logger
也就是 logging 包中的
var Logger *log.Logger
...
F, err = file.MustOpen(fileName, filePath)
...
Logger = log.New(F, DefaultPrefix, log.LstdFlags)
所以 gorm 写入日志只是调用了 log.Printf ,而饼没有经过 logging 封装的方法,这就导致 gorm 写入的日志的日志等级和前缀没有办法设置,于是就是上一次写入日志时设置的日志等级和前缀,而日志组件是系统启动是就启动的,也就是说所以的goroutine共用的。但是这个关系也不大,因为 gorm 的日志内容里还有自己的错误级别,你可以据此判断错误情况。
所有的sql语句都是由 callbacks.go
下的 Execute
方法来执行的,在这个方法中执行的日志记录
db.Logger.Trace(stmt.Context, curTime, func() (string, int64) {
return db.Dialector.Explain(stmt.SQL.String(), stmt.Vars...), db.RowsAffected
}, db.Error)
对应的就是 logger 下面的 Trace
方法:
func (l logger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
if l.LogLevel > 0 {
elapsed := time.Since(begin)
switch {
case err != nil && l.LogLevel >= Error:
sql, rows := fc()
l.Printf(l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql)
case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= Warn:
sql, rows := fc()
l.Printf(l.traceWarnStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql)
case l.LogLevel >= Info:
sql, rows := fc()
l.Printf(l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql)
}
}
}
在这个方法中你可以看出在哪些情况下会记录日志。
这三个 case 中都会记录sql语句,1和2是异常的情况,3是正常的情况。
对于慢sql日志,需要设置 SlowThreshold
大于0的数,且 LogLevel >= Warn
。
对于err,需要设置 LogLevel >= Error
。
注意 gorm 的日志级别递进关系:
Silent 1
Error 2
Warn 3
Info 4
所以,如果想要所有的sql都会被记录下来,那么 LogLevel 应该设置为 Info 。
如果只是想调试单个操作的sql的话,可以连接上一个 Debug() 操作:
db.Debug().Where("name = ?", "jinzhu").First(&User{})
我们来看一下 Debug() 方法:
func (db *DB) Debug() (tx *DB) {
return db.Session(&Session{
WithConditions: true,
Logger: db.Logger.LogMode(logger.Info),
})
}
很明显它是一次性的。
指的注意的是,当我们查询单条记录的时候,我们通过 ErrRecordNotFound
来判断是否存在,为什么会这样呢?
我们来看一下 scan.go
文件,它是将查询出的结果放入目标对象,而由于golang变量的零值的存在,所以不好判断是否查询到结果,于是就定义了一个err:
...
if db.RowsAffected == 0 && db.Statement.RaiseErrorOnNotFound {
db.AddError(ErrRecordNotFound)
}
然后来看 finisher_api.go
文件,着里面定义查询单条记录的方法,First, Take, Last
,它们都将 Statement.RaiseErrorOnNotFound 设置为 true
啦:
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) {
tx = db.Limit(1).Order(clause.OrderByColumn{
Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
})
if len(conds) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: tx.Statement.BuildCondition(conds[0], conds[1:]...)})
}
tx.Statement.RaiseErrorOnNotFound = true
tx.Statement.Dest = dest
tx.callbacks.Query().Execute(tx)
return
}
通过以上分析,在开发环境 LogLevel 设置为 Info,线上环境设置为 Warn 。
但是存在一个问题,那就是当查询单条记录不存在时,会将sql记录下来,因此会有很多record not found
的日志,这个设计的初衷时什么?当然,你可以不查询单条,改成Limit(1).Find()
查询多条就不会有record not found
的日志了,有两种方式。
1、传入切片
var book []ResourceModel
model.Where("xxxx", xxxx).Limit(1).Find(&book)
if len(book) > 0 {
fmt.Println(book[0])
}
2、传入模型,会自动取第0条返回
var book ResourceModel
model.Where("xxxx", xxxx).Limit(1).Find(&book)
fmt.Println(book)
但是在v2.0版本中,logger.Config
做了修改
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Silent, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: false, // 禁用彩色打印
},
这个改动比较符合大家的使用需求。