172 lines
5.0 KiB
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 ©
|
|
}
|
|
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)
|