250 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			250 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Package logs 提供了高度可配置的日志功能,基于 uber-go/zap 实现。
 | 
						||
// 它支持将日志同时输出到控制台和文件,并提供日志滚动归档功能。
 | 
						||
// 该包还特别为 Gin 和 GORM 框架提供了开箱即用的日志接管能力。
 | 
						||
package logs
 | 
						||
 | 
						||
import (
 | 
						||
	"context"
 | 
						||
	"fmt"
 | 
						||
	"io"
 | 
						||
	"os"
 | 
						||
	"strings"
 | 
						||
	"time"
 | 
						||
 | 
						||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
 | 
						||
	"go.uber.org/zap"
 | 
						||
	"go.uber.org/zap/buffer"
 | 
						||
	"go.uber.org/zap/zapcore"
 | 
						||
	"gopkg.in/natefinch/lumberjack.v2"
 | 
						||
	gormlogger "gorm.io/gorm/logger"
 | 
						||
)
 | 
						||
 | 
						||
// ANSI 颜色代码常量
 | 
						||
const (
 | 
						||
	reset   = "\033[0m"
 | 
						||
	red     = "\033[31m"
 | 
						||
	green   = "\033[32m"
 | 
						||
	yellow  = "\033[33m"
 | 
						||
	blue    = "\033[34m"
 | 
						||
	magenta = "\033[35m"
 | 
						||
	cyan    = "\033[36m"
 | 
						||
	white   = "\033[37m"
 | 
						||
	bold    = "\033[1m"
 | 
						||
)
 | 
						||
 | 
						||
// 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 (由 coloredConsoleEncoder 处理颜色)
 | 
						||
 | 
						||
	if format == "json" {
 | 
						||
		return zapcore.NewJSONEncoder(encoderConfig)
 | 
						||
	}
 | 
						||
	// 默认或 "console" 格式,使用自定义的带颜色编码器
 | 
						||
	return NewColoredConsoleEncoder(encoderConfig)
 | 
						||
}
 | 
						||
 | 
						||
// coloredConsoleEncoder 是一个自定义的 zapcore.Encoder,用于为整个日志行添加颜色。
 | 
						||
type coloredConsoleEncoder struct {
 | 
						||
	zapcore.Encoder // 嵌入默认的 ConsoleEncoder,以便委托其大部分功能
 | 
						||
	cfg             zapcore.EncoderConfig
 | 
						||
}
 | 
						||
 | 
						||
// NewColoredConsoleEncoder 创建一个新的 coloredConsoleEncoder 实例。
 | 
						||
func NewColoredConsoleEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
 | 
						||
	// 创建一个标准的 ConsoleEncoder 作为基础
 | 
						||
	baseEncoder := zapcore.NewConsoleEncoder(cfg)
 | 
						||
	return &coloredConsoleEncoder{
 | 
						||
		Encoder: baseEncoder,
 | 
						||
		cfg:     cfg,
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
// EncodeEntry 重写 EncodeEntry 方法,在原始日志输出前后添加颜色代码。
 | 
						||
func (c *coloredConsoleEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
 | 
						||
	// 首先,让嵌入的默认编码器生成原始的日志行
 | 
						||
	buf, err := c.Encoder.EncodeEntry(entry, fields)
 | 
						||
	if err != nil {
 | 
						||
		return nil, err
 | 
						||
	}
 | 
						||
 | 
						||
	// 根据日志级别确定颜色
 | 
						||
	var color string
 | 
						||
	switch entry.Level {
 | 
						||
	case zapcore.DebugLevel:
 | 
						||
		color = blue
 | 
						||
	case zapcore.InfoLevel:
 | 
						||
		color = green
 | 
						||
	case zapcore.WarnLevel:
 | 
						||
		color = yellow
 | 
						||
	case zapcore.ErrorLevel:
 | 
						||
		color = red
 | 
						||
	case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
 | 
						||
		color = bold + red
 | 
						||
	default:
 | 
						||
		color = reset
 | 
						||
	}
 | 
						||
 | 
						||
	// 创建一个新的 buffer 来存储带颜色的日志行
 | 
						||
	coloredBuf := buffer.NewPool().Get()
 | 
						||
	coloredBuf.AppendString(color)
 | 
						||
	coloredBuf.Write(buf.Bytes())
 | 
						||
	coloredBuf.AppendString(reset)
 | 
						||
 | 
						||
	buf.Free() // 释放原始 buffer 回池中
 | 
						||
 | 
						||
	return coloredBuf, nil
 | 
						||
}
 | 
						||
 | 
						||
// Clone 方法也需要重写,以确保返回一个新的 coloredConsoleEncoder 实例。
 | 
						||
func (c *coloredConsoleEncoder) Clone() zapcore.Encoder {
 | 
						||
	return NewColoredConsoleEncoder(c.cfg)
 | 
						||
}
 | 
						||
 | 
						||
// 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")
 | 
						||
	// --- 逻辑修复结束 ---
 | 
						||
}
 | 
						||
 | 
						||
// NewSilentLogger 创建一个不输出任何日志的 Logger 实例, 用于测试中屏蔽日志
 | 
						||
func NewSilentLogger() *Logger {
 | 
						||
	// 创建一个不输出日志的真实 logs.Logger 实例
 | 
						||
	discardSyncer := zapcore.AddSync(io.Discard)
 | 
						||
	encoderConfig := zap.NewProductionEncoderConfig()
 | 
						||
	encoder := zapcore.NewConsoleEncoder(encoderConfig)
 | 
						||
	core := zapcore.NewCore(encoder, discardSyncer, zap.DebugLevel) // 设置为 DebugLevel 以确保所有日志都被处理(并丢弃)
 | 
						||
	zapLogger := zap.New(core)
 | 
						||
	sugaredLogger := zapLogger.Sugar()
 | 
						||
	return &Logger{SugaredLogger: sugaredLogger}
 | 
						||
}
 |