166 lines
5.5 KiB
Go
166 lines
5.5 KiB
Go
// Package logs 提供了高度可配置的日志功能,基于 uber-go/zap 实现。
|
||
// 它支持将日志同时输出到控制台和文件,并提供日志滚动归档功能。
|
||
// 该包还特别为 Gin 和 GORM 框架提供了开箱即用的日志接管能力。
|
||
package logs
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
|
||
"go.uber.org/zap"
|
||
"go.uber.org/zap/zapcore"
|
||
"gopkg.in/natefinch/lumberjack.v2"
|
||
gormlogger "gorm.io/gorm/logger"
|
||
)
|
||
|
||
// Logger 是一个封装了 zap.SugaredLogger 的日志记录器。
|
||
// 它提供了结构化日志记录的各种方法,并实现了 io.Writer 接口以兼容 Gin。
|
||
type Logger struct {
|
||
*zap.SugaredLogger
|
||
}
|
||
|
||
// NewLogger 根据提供的配置创建一个新的 Logger 实例。
|
||
// 这是实现依赖注入的关键,在应用启动时调用一次。
|
||
func NewLogger(cfg config.LogConfig) *Logger {
|
||
// 1. 设置日志编码器
|
||
encoder := getEncoder(cfg.Format)
|
||
|
||
// 2. 设置日志写入器 (支持文件和控制台)
|
||
writeSyncer := getWriteSyncer(cfg)
|
||
|
||
// 3. 设置日志级别
|
||
level := zap.NewAtomicLevel()
|
||
if err := level.UnmarshalText([]byte(cfg.Level)); err != nil {
|
||
level.SetLevel(zap.InfoLevel) // 解析失败则默认为 Info 级别
|
||
}
|
||
|
||
// 4. 创建 Zap 核心
|
||
core := zapcore.NewCore(encoder, writeSyncer, level)
|
||
|
||
// 5. 构建 Logger
|
||
// zap.AddCaller() 会记录调用日志的代码行
|
||
// zap.AddCallerSkip(1) 可以向上跳一层调用栈,如果我们将 logger.Info 等方法再封装一层,这个选项会很有用
|
||
zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
|
||
|
||
return &Logger{zapLogger.Sugar()}
|
||
}
|
||
|
||
// getEncoder 根据指定的格式返回一个 zapcore.Encoder。
|
||
func getEncoder(format string) zapcore.Encoder {
|
||
encoderConfig := zap.NewProductionEncoderConfig()
|
||
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 时间格式: 2006-01-02T15:04:05.000Z0700
|
||
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 日志级别大写: INFO
|
||
|
||
if format == "json" {
|
||
return zapcore.NewJSONEncoder(encoderConfig)
|
||
}
|
||
// 默认或 "console"
|
||
return zapcore.NewConsoleEncoder(encoderConfig)
|
||
}
|
||
|
||
// getWriteSyncer 根据配置创建日志写入目标。
|
||
func getWriteSyncer(cfg config.LogConfig) zapcore.WriteSyncer {
|
||
writers := []zapcore.WriteSyncer{os.Stdout}
|
||
|
||
if cfg.EnableFile {
|
||
// 使用 lumberjack 实现日志滚动
|
||
fileWriter := &lumberjack.Logger{
|
||
Filename: cfg.FilePath,
|
||
MaxSize: cfg.MaxSize,
|
||
MaxBackups: cfg.MaxBackups,
|
||
MaxAge: cfg.MaxAge,
|
||
Compress: cfg.Compress,
|
||
}
|
||
writers = append(writers, zapcore.AddSync(fileWriter))
|
||
}
|
||
|
||
return zapcore.NewMultiWriteSyncer(writers...)
|
||
}
|
||
|
||
// Write 实现了 io.Writer 接口,用于接管 Gin 的默认输出。
|
||
// Gin 的日志(如 [GIN-debug] Listening and serving HTTP on :8080)会通过这个方法写入。
|
||
func (l *Logger) Write(p []byte) (n int, err error) {
|
||
msg := strings.TrimSpace(string(p))
|
||
if msg != "" {
|
||
l.Info(msg) // 使用我们自己的 logger 来打印 Gin 的日志
|
||
}
|
||
return len(p), nil
|
||
}
|
||
|
||
// --- GORM 日志适配器 ---
|
||
|
||
// GormLogger 是一个实现了 gormlogger.Interface 的适配器,
|
||
// 它将 GORM 的日志重定向到我们的 zap Logger 中。
|
||
type GormLogger struct {
|
||
ZapLogger *Logger
|
||
SlowThreshold time.Duration
|
||
SkipErrRecordNotFound bool // 是否跳过 "record not found" 错误
|
||
}
|
||
|
||
// NewGormLogger 创建一个新的 GORM 日志记录器实例。
|
||
func NewGormLogger(zapLogger *Logger) *GormLogger {
|
||
return &GormLogger{
|
||
ZapLogger: zapLogger,
|
||
SlowThreshold: 200 * time.Millisecond, // 慢查询阈值,超过200ms则警告
|
||
SkipErrRecordNotFound: true, // 通常我们不关心 "record not found" 错误
|
||
}
|
||
}
|
||
|
||
// LogMode 设置日志模式,这里我们总是使用 zap 的级别控制,所以这个方法可以为空。
|
||
func (g *GormLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface {
|
||
// GORM 的 LogLevel 在这里不起作用,因为我们完全由 Zap 控制
|
||
return g
|
||
}
|
||
|
||
// Info 打印 Info 级别的日志。
|
||
func (g *GormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||
g.ZapLogger.Infof(msg, data...)
|
||
}
|
||
|
||
// Warn 打印 Warn 级别的日志。
|
||
func (g *GormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||
g.ZapLogger.Warnf(msg, data...)
|
||
}
|
||
|
||
// Error 打印 Error 级别的日志。
|
||
func (g *GormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
|
||
g.ZapLogger.Errorf(msg, data...)
|
||
}
|
||
|
||
// Trace 打印 SQL 查询日志,这是 GORM 日志的核心。
|
||
func (g *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||
elapsed := time.Since(begin)
|
||
sql, rows := fc()
|
||
|
||
fields := []interface{}{
|
||
"sql", sql,
|
||
"rows", rows,
|
||
"elapsed", fmt.Sprintf("%.3fms", float64(elapsed.Nanoseconds())/1e6),
|
||
}
|
||
|
||
// --- 逻辑修复开始 ---
|
||
if err != nil {
|
||
// 如果是 "record not found" 错误且我们配置了跳过,则直接返回
|
||
if g.SkipErrRecordNotFound && strings.Contains(err.Error(), "record not found") {
|
||
return
|
||
}
|
||
// 否则,记录为错误日志
|
||
g.ZapLogger.With(fields...).Errorf("[GORM] error: %s", err)
|
||
return
|
||
}
|
||
|
||
// 如果查询时间超过慢查询阈值,则记录警告
|
||
if g.SlowThreshold != 0 && elapsed > g.SlowThreshold {
|
||
g.ZapLogger.With(fields...).Warnf("[GORM] slow query")
|
||
return
|
||
}
|
||
|
||
// 正常情况,记录 Debug 级别的 SQL 查询
|
||
g.ZapLogger.With(fields...).Debugf("[GORM] trace")
|
||
// --- 逻辑修复结束 ---
|
||
}
|