本文共 3790 字,大约阅读时间需要 12 分钟。
记录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 包的封装,
于是可以这样:
newLogger := logger.New( logging.Logger, 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 1Error 2Warn 3Info 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,线上环境设置为 Warm 。
但是存在一个问题,那就是当查询单条记录不存在时,会将sql记录下来,这个设计的初衷时什么?当然,你可以不查询单条,改成 Find 查询多条,最后判断切片的长度,取第0个。
转载地址:http://njaui.baihongyu.com/