feat: add optional GORM pool and SQL logging adapter
This commit is contained in:
171
database/gormx/logger.go
Normal file
171
database/gormx/logger.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package gormx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
"gorm.io/gorm"
|
||||
gormlog "gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type LogConfig struct {
|
||||
Level string `json:"level"` // silent, error, warn (default), info
|
||||
SlowThreshold time.Duration `json:"slowThreshold"` // zero disables slow-query events
|
||||
IgnoreRecordNotFound bool `json:"ignoreRecordNotFound"`
|
||||
IncludeParameters bool `json:"includeParameters"`
|
||||
IncludeErrorDetails bool `json:"includeErrorDetails"`
|
||||
MaxSQLBytes int `json:"maxSqlBytes"` // zero uses 4096; bounds emitted SQL text
|
||||
}
|
||||
|
||||
func DefaultLogConfig() LogConfig {
|
||||
return LogConfig{Level: "warn", SlowThreshold: 200 * time.Millisecond, IgnoreRecordNotFound: true, MaxSQLBytes: 4096}
|
||||
}
|
||||
func (c LogConfig) Validate() error {
|
||||
switch c.Level {
|
||||
case "", "silent", "error", "warn", "info":
|
||||
default:
|
||||
return errors.New("gormx: invalid SQL log level")
|
||||
}
|
||||
if c.SlowThreshold < 0 || c.MaxSQLBytes < 0 {
|
||||
return errors.New("gormx: SQL log limits cannot be negative")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type sqlLogger struct {
|
||||
logger *logx.Logger
|
||||
config LogConfig
|
||||
level gormlog.LogLevel
|
||||
}
|
||||
|
||||
// NewLogger adapts logx without changing its level, globals or ownership.
|
||||
// Bound SQL arguments and raw driver error text are omitted by default. SQL
|
||||
// literals written directly into a query are not parsed or redacted.
|
||||
func NewLogger(logger *logx.Logger, config LogConfig) (gormlog.Interface, error) {
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.MaxSQLBytes == 0 {
|
||||
config.MaxSQLBytes = 4096
|
||||
}
|
||||
level := gormlog.Warn
|
||||
switch config.Level {
|
||||
case "silent":
|
||||
level = gormlog.Silent
|
||||
case "error":
|
||||
level = gormlog.Error
|
||||
case "info":
|
||||
level = gormlog.Info
|
||||
}
|
||||
return &sqlLogger{logger: logger, config: config, level: level}, nil
|
||||
}
|
||||
func (l *sqlLogger) LogMode(level gormlog.LogLevel) gormlog.Interface {
|
||||
copy := *l
|
||||
if level < gormlog.Silent || level > gormlog.Info {
|
||||
level = gormlog.Silent
|
||||
}
|
||||
copy.level = level
|
||||
return ©
|
||||
}
|
||||
func (l *sqlLogger) output(ctx context.Context) *logx.Logger {
|
||||
if l.logger != nil {
|
||||
return l.logger
|
||||
}
|
||||
return logx.FromContext(ctx)
|
||||
}
|
||||
func (l *sqlLogger) message(msg string, args []any) string {
|
||||
if !l.config.IncludeErrorDetails {
|
||||
copy := append([]any(nil), args...)
|
||||
for i, v := range copy {
|
||||
if e, ok := v.(error); ok {
|
||||
copy[i] = errorKind(e)
|
||||
}
|
||||
}
|
||||
args = copy
|
||||
}
|
||||
return clip(fmt.Sprintf(msg, args...), l.config.MaxSQLBytes)
|
||||
}
|
||||
func (l *sqlLogger) Info(ctx context.Context, msg string, args ...any) {
|
||||
if l.level >= gormlog.Info {
|
||||
l.output(ctx).InfoContext(ctx, "gorm.message", "message", l.message(msg, args))
|
||||
}
|
||||
}
|
||||
func (l *sqlLogger) Warn(ctx context.Context, msg string, args ...any) {
|
||||
if l.level >= gormlog.Warn {
|
||||
l.output(ctx).WarnContext(ctx, "gorm.message", "message", l.message(msg, args))
|
||||
}
|
||||
}
|
||||
func (l *sqlLogger) Error(ctx context.Context, msg string, args ...any) {
|
||||
if l.level >= gormlog.Error {
|
||||
l.output(ctx).ErrorContext(ctx, "gorm.message", "message", l.message(msg, args))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *sqlLogger) ParamsFilter(_ context.Context, query string, args ...any) (string, []any) {
|
||||
if !l.config.IncludeParameters {
|
||||
return query, nil
|
||||
}
|
||||
return query, args
|
||||
}
|
||||
func (l *sqlLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
||||
if l.level == gormlog.Silent {
|
||||
return
|
||||
}
|
||||
elapsed := time.Since(begin)
|
||||
failure := err != nil && !(l.config.IgnoreRecordNotFound && errors.Is(err, gorm.ErrRecordNotFound))
|
||||
slow := l.config.SlowThreshold > 0 && elapsed > l.config.SlowThreshold
|
||||
level := gormlog.Info
|
||||
if failure && l.level >= gormlog.Error {
|
||||
level = gormlog.Error
|
||||
} else if slow && l.level >= gormlog.Warn {
|
||||
level = gormlog.Warn
|
||||
} else if l.level < gormlog.Info {
|
||||
return
|
||||
}
|
||||
query, rows := fc()
|
||||
attrs := []any{"durationMs", float64(elapsed) / float64(time.Millisecond), "rowsAffected", rows, "sql", clip(query, l.config.MaxSQLBytes)}
|
||||
if level == gormlog.Error {
|
||||
attrs = append(attrs, "errorKind", errorKind(err))
|
||||
if l.config.IncludeErrorDetails {
|
||||
attrs = append(attrs, "error", clip(err.Error(), l.config.MaxSQLBytes))
|
||||
}
|
||||
}
|
||||
logger := l.output(ctx)
|
||||
switch level {
|
||||
case gormlog.Error:
|
||||
logger.ErrorContext(ctx, "gorm.queryFailed", attrs...)
|
||||
case gormlog.Warn:
|
||||
logger.WarnContext(ctx, "gorm.slowQuery", attrs...)
|
||||
default:
|
||||
logger.InfoContext(ctx, "gorm.query", attrs...)
|
||||
}
|
||||
}
|
||||
func errorKind(err error) string {
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
return "canceled"
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "deadlineExceeded"
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
return "recordNotFound"
|
||||
default:
|
||||
return fmt.Sprintf("%T", err)
|
||||
}
|
||||
}
|
||||
func clip(value string, limit int) string {
|
||||
if len(value) <= limit {
|
||||
return value
|
||||
}
|
||||
value = value[:limit]
|
||||
for !utf8.ValidString(value) && len(value) > 0 {
|
||||
value = value[:len(value)-1]
|
||||
}
|
||||
return value + "…"
|
||||
}
|
||||
|
||||
var _ gormlog.Interface = (*sqlLogger)(nil)
|
||||
var _ gorm.ParamsFilter = (*sqlLogger)(nil)
|
||||
Reference in New Issue
Block a user