SQL Hooks
SQLHooks 的原理非常简单,封装了一个 Driver 实现原生库 driver.Driver,在调用 Exec、Query 以及 Prepare 等操作函数时调用开发者传入的钩子函数。
利用 SQL Hooks 在 sql.Driver 上挂载钩子函数
使用了第三方 hooks 库 GitHub-sqlhooks的Warp
方法来生成driver
注册到SQL的SDK
三方 hooks 定义的接口
// Driver implements a database/sql/driver.Driver
type Driver struct {
driver.Driver
hooks Hooks
}
// Hooks instances may be passed to Wrap() to define an instrumented driver
type Hooks interface {
Before(ctx context.Context, query string, args ...interface{}) (context.Context, error)
After(ctx context.Context, query string, args ...interface{}) (context.Context, error)
}
定义结构体,实现driver接口
// make sure zapHook implement all sqlhooks interface.
var _ interface {
sqlhooks.Hooks
sqlhooks.OnErrorer
} = (*zapHook)(nil)
// zapHook using zap log sql query and args.
type zapHook struct {
*zap.Logger
// 是否打印 SQL 耗时
IsPrintSQLDuration bool
}
// sqlDurationKey is context.valueCtx Key.
type sqlDurationKey struct{}
// 构造SQL语句
func buildQueryArgsFields(query string, args ...interface{}) []zap.Field {
if len(args) == 0 {
return []zap.Field{zap.String("query", query)}
}
return []zap.Field{zap.String("query", query), zap.Any("args", args)}
}
func (z *zapHook) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
if z == nil || z.Logger == nil {
return ctx, nil
}
z.Info("log before sql exec", buildQueryArgsFields(query, args...)...)
if z.IsPrintSQLDuration {
ctx = context.WithValue(ctx, (*sqlDurationKey)(nil), time.Now())
}
return ctx, nil
}
func (z *zapHook) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
if z == nil || z.Logger == nil {
return ctx, nil
}
var durationField = zap.Skip()
if v, ok := ctx.Value((*sqlDurationKey)(nil)).(time.Time); ok {
durationField = zap.Duration("duration", time.Now().Sub(v))
}
z.With(durationField).Info("log after sql exec", buildQueryArgsFields(query, args...)...)
return ctx, nil
}
// 重要:出错的时候必定要执行的参数
func (z *zapHook) OnError(_ context.Context, err error, query string, args ...interface{}) error {
if z == nil || z.Logger == nil {
return nil
}
z.With(zap.Error(err)).Error("log after err happened", buildQueryArgsFields(query, args...)...)
return nil
}
使用hooks注册到SDK
// 大部分 MySQL 操作都使用 go-sql-driver 作为驱动.
// 这个驱动执行的时候,会有一个如下的警告,会在 OnError 方法中输出
// skip fast-path; continue as if unimplemented
import (
"database/sql"
"github.com/go-sql-driver/mysql"
)
// 覆盖驱动名 mysql 会导致 panic, 因此需要创建新的驱动.
//
// database/sql/sql.go:51
const driverName = "mysql-zap"
// 用的时候,先调用上函数初始化一下
func initZapHook(log *zap.Logger) {
if log == nil {
log = zap.L()
}
hook := &zapHook{Logger: log, IsPrintSQLDuration: true}
sql.Register(zapDriverName, sqlhooks.Wrap(new(mysql.MySQLDriver), hook))
}
SQLX 的实践
- 初始化-注册driver
// 初始化调用一个注册到SDK上 hooks.InitMysqlZapHook(zapLogger, global.App.Cfg.Mysql)
- 链接数据库时,用注册好的driver名,最好使用
sqlx.ConnectContext
来链接数据库
- 业务中执行查询操作
- 输出SQL如下
问题
skip fast-path; continue as if unimplemented
这应该是使用库github.com/go-sql-driver/mysql
时报的,解决
方案一
在链接数据库的DSN上添加参数:interpolateParams=true
方案二
所有查询都先用DB.Prepare
和Stmt.Exec
来操作
参考:
Github-go-sql-driver/mysql/issues/413
go-sql-driver interpolateparams参数优化
go mysql driver InterpolateParams
gorm mysql 报错 driver skip fast-path; continue as if unimplemented
参考
Go 基于原生库驱动 driver 输出 SQL 日志
Github - sqlhooks-example
Github - SQLHooks
Go 语言设计与实现 - 数据库
Github - SQLX