Golang的Sqlx库实现SQL日志输出,其它应该库应该也可以

SQL Hooks

SQLHooks 的原理非常简单,封装了一个 Driver 实现原生库 driver.Driver,在调用 Exec、Query 以及 Prepare 等操作函数时调用开发者传入的钩子函数。

利用 SQL Hooks 在 sql.Driver 上挂载钩子函数

使用了第三方 hooks 库 GitHub-sqlhooksWarp方法来生成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 的实践

  1. 初始化-注册driver
    // 初始化调用一个注册到SDK上
    hooks.InitMysqlZapHook(zapLogger, global.App.Cfg.Mysql)
  2. 链接数据库时,用注册好的driver名,最好使用 sqlx.ConnectContext来链接数据库
    file
  3. 业务中执行查询操作
    file
  4. 输出SQL如下
    file

    问题

    skip fast-path; continue as if unimplemented
    这应该是使用库github.com/go-sql-driver/mysql时报的,

    解决

    方案一
    在链接数据库的DSN上添加参数:interpolateParams=true
    方案二
    所有查询都先用DB.PrepareStmt.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