From f7fa82ae57e76a0c4a2e4122d86b2de97e6e0ec1 Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Mon, 27 Jul 2026 20:35:59 +0800 Subject: [PATCH 1/4] fix: parameterize slow/error SQL logs to avoid leaking credentials --- model/gorm_logger.go | 29 +++++++++++++++++++++++++++++ model/main.go | 20 +++++--------------- 2 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 model/gorm_logger.go diff --git a/model/gorm_logger.go b/model/gorm_logger.go new file mode 100644 index 000000000000..cfb291e09f73 --- /dev/null +++ b/model/gorm_logger.go @@ -0,0 +1,29 @@ +package model + +import ( + "log" + "os" + "time" + + "github.com/QuantumNous/new-api/common" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func newGormConfig(prepareStmt bool) *gorm.Config { + return &gorm.Config{ + PrepareStmt: prepareStmt, + Logger: newGormLogger(), + } +} + +func newGormLogger() logger.Interface { + slowThresholdMs := common.GetEnvOrDefault("SQL_SLOW_THRESHOLD_MS", 200) + return logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ + SlowThreshold: time.Duration(slowThresholdMs) * time.Millisecond, + LogLevel: logger.Warn, + IgnoreRecordNotFoundError: true, + ParameterizedQueries: !common.DebugEnabled, + Colorful: true, + }) +} diff --git a/model/main.go b/model/main.go index ac63d1f59c12..21445593e54e 100644 --- a/model/main.go +++ b/model/main.go @@ -132,9 +132,7 @@ func chooseDB(envName string, isLog bool) (*gorm.DB, common.DatabaseType, error) return nil, "", fmt.Errorf("%s does not support ClickHouse; use SQLite, MySQL, or PostgreSQL for the primary database and LOG_SQL_DSN for ClickHouse logs", envName) } common.SysLog("using ClickHouse as log database") - db, err := gorm.Open(clickhouse.Open(normalizeClickHouseDSN(dsn)), &gorm.Config{ - PrepareStmt: false, - }) + db, err := gorm.Open(clickhouse.Open(normalizeClickHouseDSN(dsn)), newGormConfig(false)) return db, common.DatabaseTypeClickHouse, err } if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { @@ -143,16 +141,12 @@ func chooseDB(envName string, isLog bool) (*gorm.DB, common.DatabaseType, error) db, err := gorm.Open(postgres.New(postgres.Config{ DSN: dsn, PreferSimpleProtocol: true, // disables implicit prepared statement usage - }), &gorm.Config{ - PrepareStmt: true, // precompile SQL - }) + }), newGormConfig(true)) return db, common.DatabaseTypePostgreSQL, err } if strings.HasPrefix(dsn, "local") { common.SysLog("SQL_DSN not set, using SQLite as database") - db, err := gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{ - PrepareStmt: true, // precompile SQL - }) + db, err := gorm.Open(sqlite.Open(common.SQLitePath), newGormConfig(true)) return db, common.DatabaseTypeSQLite, err } // Use MySQL @@ -165,16 +159,12 @@ func chooseDB(envName string, isLog bool) (*gorm.DB, common.DatabaseType, error) dsn += "?parseTime=true" } } - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ - PrepareStmt: true, // precompile SQL - }) + db, err := gorm.Open(mysql.Open(dsn), newGormConfig(true)) return db, common.DatabaseTypeMySQL, err } // Use SQLite common.SysLog("SQL_DSN not set, using SQLite as database") - db, err := gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{ - PrepareStmt: true, // precompile SQL - }) + db, err := gorm.Open(sqlite.Open(common.SQLitePath), newGormConfig(true)) return db, common.DatabaseTypeSQLite, err } From dbb91cc96b9bd357dbc869fc2ab5cc0551b203ac Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Mon, 27 Jul 2026 21:13:36 +0800 Subject: [PATCH 2/4] fix: validate SQL_SLOW_THRESHOLD_MS range --- .env.example | 2 ++ model/gorm_logger.go | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index d62e114603c0..3b8a2a5b9534 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,8 @@ # SQL_MAX_OPEN_CONNS=1000 # 数据库连接最大生命周期(秒) # SQL_MAX_LIFETIME=60 +# 慢查询日志阈值(毫秒),0 表示关闭慢查询日志,超出 0-3600000 范围回退默认值 200 +# SQL_SLOW_THRESHOLD_MS=200 # 缓存相关配置 diff --git a/model/gorm_logger.go b/model/gorm_logger.go index cfb291e09f73..0ab379f5952e 100644 --- a/model/gorm_logger.go +++ b/model/gorm_logger.go @@ -1,6 +1,7 @@ package model import ( + "fmt" "log" "os" "time" @@ -10,6 +11,11 @@ import ( "gorm.io/gorm/logger" ) +const ( + defaultSlowThresholdMs = 200 + maxSlowThresholdMs = 60 * 60 * 1000 +) + func newGormConfig(prepareStmt bool) *gorm.Config { return &gorm.Config{ PrepareStmt: prepareStmt, @@ -18,7 +24,11 @@ func newGormConfig(prepareStmt bool) *gorm.Config { } func newGormLogger() logger.Interface { - slowThresholdMs := common.GetEnvOrDefault("SQL_SLOW_THRESHOLD_MS", 200) + slowThresholdMs := common.GetEnvOrDefault("SQL_SLOW_THRESHOLD_MS", defaultSlowThresholdMs) + if slowThresholdMs < 0 || slowThresholdMs > maxSlowThresholdMs { + common.SysError(fmt.Sprintf("invalid SQL_SLOW_THRESHOLD_MS %d (allowed 0-%d, 0 disables slow query log), using default %d", slowThresholdMs, maxSlowThresholdMs, defaultSlowThresholdMs)) + slowThresholdMs = defaultSlowThresholdMs + } return logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ SlowThreshold: time.Duration(slowThresholdMs) * time.Millisecond, LogLevel: logger.Warn, From a419779a389863a59c58dc15c54240d6a238080b Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Mon, 27 Jul 2026 21:22:44 +0800 Subject: [PATCH 3/4] fix: sanitize database driver error messages in SQL logs --- go.mod | 12 +++-- model/gorm_logger.go | 64 ++++++++++++++++++++++++- model/gorm_logger_test.go | 99 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 model/gorm_logger_test.go diff --git a/go.mod b/go.mod index 470e892b6b6e..fa0632bca9f5 100644 --- a/go.mod +++ b/go.mod @@ -67,7 +67,6 @@ require ( require ( github.com/ClickHouse/ch-go v0.65.0 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.32.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/casbin/govaluate v1.10.0 // indirect github.com/go-faster/city v1.0.1 // indirect @@ -107,13 +106,13 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/go-sqlite v1.21.2 github.com/go-audio/audio v1.0.0 // indirect github.com/go-audio/riff v1.0.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 github.com/go-webauthn/x v0.1.25 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-tpm v0.9.5 // indirect @@ -121,7 +120,7 @@ require ( github.com/icza/bitio v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.9.2 // indirect + github.com/jackc/pgx/v5 v5.9.2 github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jfreymuth/vorbis v1.0.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -163,6 +162,9 @@ require ( modernc.org/sqlite v1.40.1 // indirect ) -require github.com/QuantumNous/new-api/relaykit v0.0.0 +require ( + github.com/ClickHouse/clickhouse-go/v2 v2.32.0 + github.com/QuantumNous/new-api/relaykit v0.0.0 +) replace github.com/QuantumNous/new-api/relaykit => ./relaykit diff --git a/model/gorm_logger.go b/model/gorm_logger.go index 0ab379f5952e..8c21f93acd2d 100644 --- a/model/gorm_logger.go +++ b/model/gorm_logger.go @@ -1,12 +1,19 @@ package model import ( + "context" + "errors" "fmt" + "io" "log" "os" "time" + "github.com/ClickHouse/clickhouse-go/v2/lib/proto" "github.com/QuantumNous/new-api/common" + sqlitedriver "github.com/glebarez/go-sqlite" + "github.com/go-sql-driver/mysql" + "github.com/jackc/pgx/v5/pgconn" "gorm.io/gorm" "gorm.io/gorm/logger" ) @@ -24,16 +31,69 @@ func newGormConfig(prepareStmt bool) *gorm.Config { } func newGormLogger() logger.Interface { + return newGormLoggerWithWriter(os.Stdout) +} + +func newGormLoggerWithWriter(w io.Writer) logger.Interface { slowThresholdMs := common.GetEnvOrDefault("SQL_SLOW_THRESHOLD_MS", defaultSlowThresholdMs) if slowThresholdMs < 0 || slowThresholdMs > maxSlowThresholdMs { common.SysError(fmt.Sprintf("invalid SQL_SLOW_THRESHOLD_MS %d (allowed 0-%d, 0 disables slow query log), using default %d", slowThresholdMs, maxSlowThresholdMs, defaultSlowThresholdMs)) slowThresholdMs = defaultSlowThresholdMs } - return logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ + return &sanitizedGormLogger{Interface: logger.New(log.New(w, "\r\n", log.LstdFlags), logger.Config{ SlowThreshold: time.Duration(slowThresholdMs) * time.Millisecond, LogLevel: logger.Warn, IgnoreRecordNotFoundError: true, ParameterizedQueries: !common.DebugEnabled, Colorful: true, - }) + })} +} + +// sanitizedGormLogger 在非 DEBUG 下把数据库驱动错误消息收敛为错误码: +// ParameterizedQueries 只过滤 SQL 字符串,而 MySQL 1062、PG 23505 这类 +// 驱动错误消息本身会内联数据值,同样是泄漏面。 +type sanitizedGormLogger struct { + logger.Interface +} + +func (l *sanitizedGormLogger) LogMode(level logger.LogLevel) logger.Interface { + return &sanitizedGormLogger{Interface: l.Interface.LogMode(level)} +} + +// gorm 对配置的 logger 本体做 ParamsFilter 类型断言,包装层必须转发。 +func (l *sanitizedGormLogger) ParamsFilter(ctx context.Context, sql string, params ...interface{}) (string, []interface{}) { + if pf, ok := l.Interface.(gorm.ParamsFilter); ok { + return pf.ParamsFilter(ctx, sql, params...) + } + return sql, params +} + +func (l *sanitizedGormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { + // ErrRecordNotFound 原样透传,内层 logger 依赖 errors.Is 识别并忽略它 + if err != nil && !common.DebugEnabled && !errors.Is(err, gorm.ErrRecordNotFound) { + err = sanitizeDBError(err) + } + l.Interface.Trace(ctx, begin, fc, err) +} + +// sanitizeDBError 只收敛四种数据库驱动的错误(消息由数据库服务端生成,可能内联 +// 数据值);网络/上下文等其它错误消息不含查询数据,原样保留以便运维排障。 +func sanitizeDBError(err error) error { + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) { + return fmt.Errorf("mysql error %d", mysqlErr.Number) + } + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + return fmt.Errorf("postgres error SQLSTATE %s", pgErr.Code) + } + var chErr *proto.Exception + if errors.As(err, &chErr) { + return fmt.Errorf("clickhouse error %d", chErr.Code) + } + var sqliteErr *sqlitedriver.Error + if errors.As(err, &sqliteErr) { + return fmt.Errorf("sqlite error %d", sqliteErr.Code()) + } + return err } diff --git a/model/gorm_logger_test.go b/model/gorm_logger_test.go new file mode 100644 index 000000000000..6fe5a289055e --- /dev/null +++ b/model/gorm_logger_test.go @@ -0,0 +1,99 @@ +package model + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/ClickHouse/clickhouse-go/v2/lib/proto" + "github.com/QuantumNous/new-api/common" + "github.com/glebarez/sqlite" + "github.com/go-sql-driver/mysql" + "github.com/jackc/pgx/v5/pgconn" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +// 保护契约:数据库驱动错误消息可能内联数据值,非 DEBUG 下日志只保留错误码。 +func TestSanitizeDBErrorStripsDriverMessage(t *testing.T) { + tests := []struct { + name string + err error + want string + leaked string + }{ + { + name: "mysql duplicate entry", + err: &mysql.MySQLError{Number: 1062, Message: "Duplicate entry 'secret-value' for key 'users.idx'"}, + want: "mysql error 1062", + leaked: "secret-value", + }, + { + name: "postgres unique violation", + err: &pgconn.PgError{Code: "23505", Message: "duplicate key value", Detail: "Key (k)=(secret-value) already exists."}, + want: "postgres error SQLSTATE 23505", + leaked: "secret-value", + }, + { + name: "clickhouse exception", + err: &proto.Exception{Code: 241, Message: "Memory limit exceeded while processing 'secret-value'"}, + want: "clickhouse error 241", + leaked: "secret-value", + }, + { + name: "wrapped driver error", + err: fmt.Errorf("exec failed: %w", &mysql.MySQLError{Number: 1064, Message: "syntax error near 'secret-value'"}), + want: "mysql error 1064", + leaked: "secret-value", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := sanitizeDBError(tc.err) + require.Error(t, got) + assert.Equal(t, tc.want, got.Error()) + assert.NotContains(t, got.Error(), tc.leaked) + }) + } +} + +func TestSanitizeDBErrorSQLiteDriver(t *testing.T) { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + execErr := db.Exec("INSERT INTO missing_table (k) VALUES (?)", "secret-value").Error + require.Error(t, execErr) + + got := sanitizeDBError(execErr) + assert.Regexp(t, `^sqlite error \d+$`, got.Error()) + assert.NotContains(t, got.Error(), "secret-value") +} + +func TestSanitizeDBErrorKeepsNonDriverErrors(t *testing.T) { + err := context.DeadlineExceeded + assert.Equal(t, err, sanitizeDBError(err)) +} + +// 保护契约:错误日志里 SQL 参数化、驱动错误消息脱敏同时生效;DEBUG=true 保留原文。 +func TestGormLoggerTraceSanitizedOutput(t *testing.T) { + previousDebug := common.DebugEnabled + t.Cleanup(func() { common.DebugEnabled = previousDebug }) + + driverErr := &mysql.MySQLError{Number: 1062, Message: "Duplicate entry 'secret-value' for key 'users.idx'"} + fc := func() (string, int64) { return "SELECT * FROM t WHERE k = ?", 0 } + + common.DebugEnabled = false + var buf bytes.Buffer + newGormLoggerWithWriter(&buf).Trace(context.Background(), time.Now(), fc, driverErr) + out := buf.String() + assert.Contains(t, out, "mysql error 1062") + assert.Contains(t, out, "k = ?") + assert.NotContains(t, out, "secret-value") + + common.DebugEnabled = true + buf.Reset() + newGormLoggerWithWriter(&buf).Trace(context.Background(), time.Now(), fc, driverErr) + assert.Contains(t, buf.String(), "secret-value") +} From b80df71ee1ec8ee2e6815c874151a79548222686 Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Mon, 27 Jul 2026 21:46:38 +0800 Subject: [PATCH 4/4] refactor: sanitize at gorm log writer seam to keep caller attribution --- model/gorm_logger.go | 53 +++++++++++++++------------------------ model/gorm_logger_test.go | 31 ++++++++++++----------- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/model/gorm_logger.go b/model/gorm_logger.go index 8c21f93acd2d..8143073023da 100644 --- a/model/gorm_logger.go +++ b/model/gorm_logger.go @@ -1,7 +1,6 @@ package model import ( - "context" "errors" "fmt" "io" @@ -26,58 +25,46 @@ const ( func newGormConfig(prepareStmt bool) *gorm.Config { return &gorm.Config{ PrepareStmt: prepareStmt, - Logger: newGormLogger(), + Logger: newGormLogger(os.Stdout), } } -func newGormLogger() logger.Interface { - return newGormLoggerWithWriter(os.Stdout) -} - -func newGormLoggerWithWriter(w io.Writer) logger.Interface { +func newGormLogger(w io.Writer) logger.Interface { slowThresholdMs := common.GetEnvOrDefault("SQL_SLOW_THRESHOLD_MS", defaultSlowThresholdMs) if slowThresholdMs < 0 || slowThresholdMs > maxSlowThresholdMs { common.SysError(fmt.Sprintf("invalid SQL_SLOW_THRESHOLD_MS %d (allowed 0-%d, 0 disables slow query log), using default %d", slowThresholdMs, maxSlowThresholdMs, defaultSlowThresholdMs)) slowThresholdMs = defaultSlowThresholdMs } - return &sanitizedGormLogger{Interface: logger.New(log.New(w, "\r\n", log.LstdFlags), logger.Config{ + // 在 Writer 层脱敏而非包装 logger.Interface:后者会让 gorm 的 FileWithLineNum + // 把所有 SQL 日志的调用点归因到包装层自身,且需转发 ParamsFilter 类型断言。 + return logger.New(&sanitizedLogWriter{delegate: log.New(w, "\r\n", log.LstdFlags)}, logger.Config{ SlowThreshold: time.Duration(slowThresholdMs) * time.Millisecond, LogLevel: logger.Warn, IgnoreRecordNotFoundError: true, ParameterizedQueries: !common.DebugEnabled, Colorful: true, - })} -} - -// sanitizedGormLogger 在非 DEBUG 下把数据库驱动错误消息收敛为错误码: -// ParameterizedQueries 只过滤 SQL 字符串,而 MySQL 1062、PG 23505 这类 -// 驱动错误消息本身会内联数据值,同样是泄漏面。 -type sanitizedGormLogger struct { - logger.Interface + }) } -func (l *sanitizedGormLogger) LogMode(level logger.LogLevel) logger.Interface { - return &sanitizedGormLogger{Interface: l.Interface.LogMode(level)} -} - -// gorm 对配置的 logger 本体做 ParamsFilter 类型断言,包装层必须转发。 -func (l *sanitizedGormLogger) ParamsFilter(ctx context.Context, sql string, params ...interface{}) (string, []interface{}) { - if pf, ok := l.Interface.(gorm.ParamsFilter); ok { - return pf.ParamsFilter(ctx, sql, params...) - } - return sql, params +// ParameterizedQueries 只过滤 SQL 字符串,驱动错误消息(如 MySQL 1062)同样会 +// 内联数据值,在这里收敛为错误码;DEBUG=true 保留原文。 +type sanitizedLogWriter struct { + delegate *log.Logger } -func (l *sanitizedGormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { - // ErrRecordNotFound 原样透传,内层 logger 依赖 errors.Is 识别并忽略它 - if err != nil && !common.DebugEnabled && !errors.Is(err, gorm.ErrRecordNotFound) { - err = sanitizeDBError(err) +func (s *sanitizedLogWriter) Printf(format string, args ...interface{}) { + if !common.DebugEnabled { + for i, arg := range args { + if err, ok := arg.(error); ok { + args[i] = sanitizeDBError(err) + } + } } - l.Interface.Trace(ctx, begin, fc, err) + s.delegate.Printf(format, args...) } -// sanitizeDBError 只收敛四种数据库驱动的错误(消息由数据库服务端生成,可能内联 -// 数据值);网络/上下文等其它错误消息不含查询数据,原样保留以便运维排障。 +// 只收敛数据库服务端生成的驱动错误(消息可能内联数据值);网络/上下文等 +// 其它错误不含查询数据,原样保留以便排障。 func sanitizeDBError(err error) error { var mysqlErr *mysql.MySQLError if errors.As(err, &mysqlErr) { diff --git a/model/gorm_logger_test.go b/model/gorm_logger_test.go index 6fe5a289055e..6095e8beb3f9 100644 --- a/model/gorm_logger_test.go +++ b/model/gorm_logger_test.go @@ -2,10 +2,8 @@ package model import ( "bytes" - "context" "fmt" "testing" - "time" "github.com/ClickHouse/clickhouse-go/v2/lib/proto" "github.com/QuantumNous/new-api/common" @@ -72,28 +70,33 @@ func TestSanitizeDBErrorSQLiteDriver(t *testing.T) { } func TestSanitizeDBErrorKeepsNonDriverErrors(t *testing.T) { - err := context.DeadlineExceeded + err := fmt.Errorf("dial tcp 127.0.0.1:3306: connect: connection refused") assert.Equal(t, err, sanitizeDBError(err)) } -// 保护契约:错误日志里 SQL 参数化、驱动错误消息脱敏同时生效;DEBUG=true 保留原文。 -func TestGormLoggerTraceSanitizedOutput(t *testing.T) { +// 保护契约:经 gorm 真实链路,错误日志同时满足 SQL 参数化、驱动错误脱敏、 +// 调用点归因到业务代码;DEBUG=true 恢复参数值与错误原文。 +func TestGormLoggerEndToEndSanitizedOutput(t *testing.T) { previousDebug := common.DebugEnabled t.Cleanup(func() { common.DebugEnabled = previousDebug }) - driverErr := &mysql.MySQLError{Number: 1062, Message: "Duplicate entry 'secret-value' for key 'users.idx'"} - fc := func() (string, int64) { return "SELECT * FROM t WHERE k = ?", 0 } + execQuery := func() string { + var buf bytes.Buffer + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: newGormLogger(&buf)}) + require.NoError(t, err) + db.Exec("SELECT * FROM missing_table WHERE k = ?", "secret-value") + return buf.String() + } common.DebugEnabled = false - var buf bytes.Buffer - newGormLoggerWithWriter(&buf).Trace(context.Background(), time.Now(), fc, driverErr) - out := buf.String() - assert.Contains(t, out, "mysql error 1062") + out := execQuery() assert.Contains(t, out, "k = ?") assert.NotContains(t, out, "secret-value") + assert.Contains(t, out, "sqlite error") + assert.Contains(t, out, "gorm_logger_test.go") common.DebugEnabled = true - buf.Reset() - newGormLoggerWithWriter(&buf).Trace(context.Background(), time.Now(), fc, driverErr) - assert.Contains(t, buf.String(), "secret-value") + debugOut := execQuery() + assert.Contains(t, debugOut, "secret-value") + assert.Contains(t, debugOut, "no such table") }