- NewLogger 工厂函数:支持 JSON/Console 编码、stdout/文件/多输出、lumberjack 轮转 - NewGormLogger 实现 gorm.Interface:Trace 区分错误/慢查询/正常查询 - output_stdout 用 *bool 三态处理(nil=true, true, false) - 默认值:level=info, encoding=json, max_size=100, max_backups=5, max_age=30 - 慢查询阈值 200ms,ErrRecordNotFound 不视为错误 - 编译时接口检查: var _ gormlogger.Interface = (*GormLogger)(nil) - 完整 TDD 测试覆盖 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
149 lines
3.7 KiB
Go
149 lines
3.7 KiB
Go
package logger
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
"gorm.io/gorm"
|
|
gormlogger "gorm.io/gorm/logger"
|
|
)
|
|
|
|
const slowQueryThreshold = 200 * time.Millisecond
|
|
|
|
// GormLogger implements gorm's logger.Interface backed by zap.
|
|
type GormLogger struct {
|
|
logger *zap.Logger
|
|
level zapcore.Level
|
|
silent bool
|
|
}
|
|
|
|
// Compile-time interface check.
|
|
var _ gormlogger.Interface = (*GormLogger)(nil)
|
|
|
|
// NewGormLogger creates a new GormLogger wrapping the given zap logger.
|
|
// The level string maps to zap levels; empty defaults to "warn".
|
|
// The special value "silent" suppresses all output.
|
|
func NewGormLogger(zapLogger *zap.Logger, level string) gormlogger.Interface {
|
|
lvl := parseGormLevel(level)
|
|
silent := level == "silent"
|
|
return &GormLogger{
|
|
logger: zapLogger,
|
|
level: lvl,
|
|
silent: silent,
|
|
}
|
|
}
|
|
|
|
// LogMode returns a new GormLogger with the given gorm log level.
|
|
// It does NOT mutate the receiver.
|
|
func (l *GormLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface {
|
|
newLogger := &GormLogger{
|
|
logger: l.logger,
|
|
level: l.level,
|
|
silent: l.silent,
|
|
}
|
|
|
|
switch level {
|
|
case gormlogger.Silent:
|
|
newLogger.silent = true
|
|
case gormlogger.Error:
|
|
newLogger.level = zapcore.ErrorLevel
|
|
newLogger.silent = false
|
|
case gormlogger.Warn:
|
|
newLogger.level = zapcore.WarnLevel
|
|
newLogger.silent = false
|
|
case gormlogger.Info:
|
|
newLogger.level = zapcore.InfoLevel
|
|
newLogger.silent = false
|
|
}
|
|
|
|
return newLogger
|
|
}
|
|
|
|
// Info logs at zap.InfoLevel with structured fields from key-value pairs.
|
|
func (l *GormLogger) Info(ctx context.Context, msg string, args ...any) {
|
|
if l.silent || l.level > zapcore.InfoLevel {
|
|
return
|
|
}
|
|
l.logger.Info(msg, argsToFields(args)...)
|
|
}
|
|
|
|
// Warn logs at zap.WarnLevel with structured fields from key-value pairs.
|
|
func (l *GormLogger) Warn(ctx context.Context, msg string, args ...any) {
|
|
if l.silent || l.level > zapcore.WarnLevel {
|
|
return
|
|
}
|
|
l.logger.Warn(msg, argsToFields(args)...)
|
|
}
|
|
|
|
// Error logs at zap.ErrorLevel with structured fields from key-value pairs.
|
|
func (l *GormLogger) Error(ctx context.Context, msg string, args ...any) {
|
|
if l.silent || l.level > zapcore.ErrorLevel {
|
|
return
|
|
}
|
|
l.logger.Error(msg, argsToFields(args)...)
|
|
}
|
|
|
|
// Trace logs SQL query information based on execution results.
|
|
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
|
if l.silent {
|
|
return
|
|
}
|
|
|
|
elapsed := time.Since(begin)
|
|
sql, rows := fc()
|
|
|
|
switch {
|
|
case err != nil && !errors.Is(err, gorm.ErrRecordNotFound):
|
|
l.logger.Error("gorm query error",
|
|
zap.Error(err),
|
|
zap.String("sql", sql),
|
|
zap.Int64("rows", rows),
|
|
zap.Duration("elapsed", elapsed),
|
|
)
|
|
case elapsed > slowQueryThreshold:
|
|
l.logger.Warn("gorm slow query",
|
|
zap.String("sql", sql),
|
|
zap.Int64("rows", rows),
|
|
zap.Float64("elapsed_ms", float64(elapsed.Nanoseconds())/1e6),
|
|
)
|
|
default:
|
|
if l.level > zapcore.InfoLevel {
|
|
return
|
|
}
|
|
l.logger.Info("gorm query",
|
|
zap.String("sql", sql),
|
|
zap.Int64("rows", rows),
|
|
zap.Duration("elapsed", elapsed),
|
|
)
|
|
}
|
|
}
|
|
|
|
// parseGormLevel parses a level string into a zapcore.Level.
|
|
// Defaults to zapcore.WarnLevel.
|
|
func parseGormLevel(level string) zapcore.Level {
|
|
if level == "" || level == "silent" {
|
|
return zapcore.WarnLevel
|
|
}
|
|
var lvl zapcore.Level
|
|
if err := lvl.UnmarshalText([]byte(level)); err != nil {
|
|
return zapcore.WarnLevel
|
|
}
|
|
return lvl
|
|
}
|
|
|
|
// argsToFields converts alternating key-value pairs into zap fields.
|
|
func argsToFields(args []any) []zap.Field {
|
|
fields := make([]zap.Field, 0, len(args)/2)
|
|
for i := 0; i+1 < len(args); i += 2 {
|
|
key, ok := args[i].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
fields = append(fields, zap.Any(key, args[i+1]))
|
|
}
|
|
return fields
|
|
}
|