Files
pig-farm-controller/internal/logs/logs.go
huang cf011f18f2 1. 重写logs
2. logs增加单测
3. task更换新的log实例
4. 配置文件增加日志相关配置
2025-09-11 20:37:29 +08:00

166 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package logs 提供了高度可配置的日志功能,基于 uber-go/zap 实现。
// 它支持将日志同时输出到控制台和文件,并提供日志滚动归档功能。
// 该包还特别为 Gin 和 GORM 框架提供了开箱即用的日志接管能力。
package logs
import (
"context"
"fmt"
"os"
"strings"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/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")
// --- 逻辑修复结束 ---
}