Files
go-omni/database/gormx/logger.go

172 lines
5.0 KiB
Go

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 &copy
}
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)