Integrate GORM Logger with Zerolog and Add Configuration Options for Logging and Performance (#2040)

* Integrate GORM logger with zerolog and add custom GORM configuration options

* Add GormConfig struct to group GORM-related settings

* Update debug mode instruction in config-example.yaml

Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>

---------

Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
This commit is contained in:
nadongjun 2024-08-19 18:47:52 +09:00 committed by GitHub
parent ac8491efec
commit fdc034e8ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 118 additions and 2 deletions

View File

@ -140,6 +140,23 @@ ephemeral_node_inactivity_timeout: 30m
database: database:
type: sqlite type: sqlite
# Enable debug mode. This setting requires the log.level to be set to "debug" or "trace".
debug: false
# GORM configuration settings.
gorm:
# Enable prepared statements.
prepare_stmt: true
# Enable parameterized queries.
parameterized_queries: true
# Skip logging "record not found" errors.
skip_err_record_not_found: true
# Threshold for slow queries in milliseconds.
slow_threshold: 1000
# SQLite config # SQLite config
sqlite: sqlite:
path: /var/lib/headscale/db.sqlite path: /var/lib/headscale/db.sqlite

View File

@ -426,7 +426,7 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) {
// TODO(kradalby): Integrate this with zerolog // TODO(kradalby): Integrate this with zerolog
var dbLogger logger.Interface var dbLogger logger.Interface
if cfg.Debug { if cfg.Debug {
dbLogger = logger.Default dbLogger = util.NewDBLogWrapper(&log.Logger, cfg.Gorm.SlowThreshold, cfg.Gorm.SkipErrRecordNotFound, cfg.Gorm.ParameterizedQueries)
} else { } else {
dbLogger = logger.Default.LogMode(logger.Silent) dbLogger = logger.Default.LogMode(logger.Silent)
} }
@ -447,7 +447,8 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) {
db, err := gorm.Open( db, err := gorm.Open(
sqlite.Open(cfg.Sqlite.Path), sqlite.Open(cfg.Sqlite.Path),
&gorm.Config{ &gorm.Config{
Logger: dbLogger, PrepareStmt: cfg.Gorm.PrepareStmt,
Logger: dbLogger,
}, },
) )

View File

@ -120,11 +120,22 @@ type PostgresConfig struct {
ConnMaxIdleTimeSecs int ConnMaxIdleTimeSecs int
} }
type GormConfig struct {
Debug bool
SlowThreshold time.Duration
SkipErrRecordNotFound bool
ParameterizedQueries bool
PrepareStmt bool
}
type DatabaseConfig struct { type DatabaseConfig struct {
// Type sets the database type, either "sqlite3" or "postgres" // Type sets the database type, either "sqlite3" or "postgres"
Type string Type string
Debug bool Debug bool
// Type sets the gorm configuration
Gorm GormConfig
Sqlite SqliteConfig Sqlite SqliteConfig
Postgres PostgresConfig Postgres PostgresConfig
} }
@ -486,6 +497,11 @@ func GetDatabaseConfig() DatabaseConfig {
type_ := viper.GetString("database.type") type_ := viper.GetString("database.type")
skipErrRecordNotFound := viper.GetBool("database.gorm.skip_err_record_not_found")
slowThreshold := viper.GetDuration("database.gorm.slow_threshold") * time.Millisecond
parameterizedQueries := viper.GetBool("database.gorm.parameterized_queries")
prepareStmt := viper.GetBool("database.gorm.prepare_stmt")
switch type_ { switch type_ {
case DatabaseSqlite, DatabasePostgres: case DatabaseSqlite, DatabasePostgres:
break break
@ -499,6 +515,13 @@ func GetDatabaseConfig() DatabaseConfig {
return DatabaseConfig{ return DatabaseConfig{
Type: type_, Type: type_,
Debug: debug, Debug: debug,
Gorm: GormConfig{
Debug: debug,
SkipErrRecordNotFound: skipErrRecordNotFound,
SlowThreshold: slowThreshold,
ParameterizedQueries: parameterizedQueries,
PrepareStmt: prepareStmt,
},
Sqlite: SqliteConfig{ Sqlite: SqliteConfig{
Path: util.AbsolutePathFromConfigPath( Path: util.AbsolutePathFromConfigPath(
viper.GetString("database.sqlite.path"), viper.GetString("database.sqlite.path"),

View File

@ -1,7 +1,14 @@
package util package util
import ( import (
"context"
"errors"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gorm.io/gorm"
gormLogger "gorm.io/gorm/logger"
"tailscale.com/types/logger" "tailscale.com/types/logger"
) )
@ -14,3 +21,71 @@ func TSLogfWrapper() logger.Logf {
log.Debug().Caller().Msgf(format, args...) log.Debug().Caller().Msgf(format, args...)
} }
} }
type DBLogWrapper struct {
Logger *zerolog.Logger
Level zerolog.Level
Event *zerolog.Event
SlowThreshold time.Duration
SkipErrRecordNotFound bool
ParameterizedQueries bool
}
func NewDBLogWrapper(origin *zerolog.Logger, slowThreshold time.Duration, skipErrRecordNotFound bool, parameterizedQueries bool) *DBLogWrapper {
l := &DBLogWrapper{
Logger: origin,
Level: origin.GetLevel(),
SlowThreshold: slowThreshold,
SkipErrRecordNotFound: skipErrRecordNotFound,
ParameterizedQueries: parameterizedQueries,
}
return l
}
type DBLogWrapperOption func(*DBLogWrapper)
func (l *DBLogWrapper) LogMode(gormLogger.LogLevel) gormLogger.Interface {
return l
}
func (l *DBLogWrapper) Info(ctx context.Context, msg string, data ...interface{}) {
l.Logger.Info().Msgf(msg, data...)
}
func (l *DBLogWrapper) Warn(ctx context.Context, msg string, data ...interface{}) {
l.Logger.Warn().Msgf(msg, data...)
}
func (l *DBLogWrapper) Error(ctx context.Context, msg string, data ...interface{}) {
l.Logger.Error().Msgf(msg, data...)
}
func (l *DBLogWrapper) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
elapsed := time.Since(begin)
sql, rowsAffected := fc()
fields := map[string]interface{}{
"duration": elapsed,
"sql": sql,
"rowsAffected": rowsAffected,
}
if err != nil && !(errors.Is(err, gorm.ErrRecordNotFound) && l.SkipErrRecordNotFound) {
l.Logger.Error().Err(err).Fields(fields).Msgf("")
return
}
if l.SlowThreshold != 0 && elapsed > l.SlowThreshold {
l.Logger.Warn().Fields(fields).Msgf("")
return
}
l.Logger.Debug().Fields(fields).Msgf("")
}
func (l *DBLogWrapper) ParamsFilter(ctx context.Context, sql string, params ...interface{}) (string, []interface{}) {
if l.ParameterizedQueries {
return sql, nil
}
return sql, params
}