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)