feat: 添加 zap 日志工厂和 GORM 日志桥接
- 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>
This commit is contained in:
148
internal/logger/gorm.go
Normal file
148
internal/logger/gorm.go
Normal file
@@ -0,0 +1,148 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user