gorm中关于sql日志记录 golang

记录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,         // 禁用彩色打印
 },

这个改动比较符合大家的使用需求。

你可能感兴趣的:(golang,golang)