diff --git a/internal/infra/logs/context.go b/internal/infra/logs/context.go new file mode 100644 index 0000000..32b875c --- /dev/null +++ b/internal/infra/logs/context.go @@ -0,0 +1,63 @@ +package logs + +import ( + "context" + "fmt" +) + +// contextKey 是用于在 context.Context 中存储值的私有类型,避免键冲突。 +type contextKey string + +const ( + // compNameKey 用于存储组件名称。 + compNameKey contextKey = "compName" + // chainKey 用于存储调用链。 + chainKey contextKey = "chain" +) + +// AddCompName 将一个组件名(对象名)存入 Context,并返回一个包含该信息的新 Context。 +// 这通常在依赖注入时完成,用于创建组件的“身份名牌” selfCtx。 +func AddCompName(ctx context.Context, compName string) context.Context { + return context.WithValue(ctx, compNameKey, compName) +} + +// AddFuncName 这是构建调用链的核心原子操作。它智能地合并上游的调用链和当前组件的信息, +// 生成并返回一个包含更新后调用链和当前组件名的 新 Context。 +// 此函数用于只需要传递调用链而不需要立即打印日志的场景。 +func AddFuncName(upstreamCtx context.Context, selfCtx context.Context, funcName string) context.Context { + // 1. 获取上游调用链 + var oldChain []string + if val := upstreamCtx.Value(chainKey); val != nil { + if chain, ok := val.([]string); ok { + oldChain = chain + } + } + + // 2. 获取当前组件名 + compName, ok := selfCtx.Value(compNameKey).(string) + if !ok { + // 如果 selfCtx 中没有 compName,则无法构建节点,直接返回 upstreamCtx + // 这种情况通常不应该发生,因为 selfCtx 应该在依赖注入时通过 AddCompName 初始化 + return upstreamCtx + } + + // 3. 构建新节点 + newNode := fmt.Sprintf("%s.%s", compName, funcName) + + // 4. 生成新调用链 + // 创建一个新的切片,并将旧链和新节点复制进去 + newChain := make([]string, len(oldChain)+1) + copy(newChain, oldChain) + newChain[len(oldChain)] = newNode + + // 5. 创建新的 Context + // 使用 context.WithValue,以 chainKey 为键,将 newChain 存入一个新的 Context,我们称之为 tmpCtx。 + tmpCtx := context.WithValue(upstreamCtx, chainKey, newChain) + + // 接着,基于 tmpCtx,再次调用 context.WithValue,以 compNameKey 为键, + // 将从 selfCtx 中获取的 compName 存入,得到最终的 newCtx。 + // 这确保了传向下游的 Context 正确地标识了当前组件。 + newCtx := context.WithValue(tmpCtx, compNameKey, compName) + + return newCtx +} diff --git a/internal/infra/logs/logs.go b/internal/infra/logs/logs.go index 0b758eb..c206bec 100644 --- a/internal/infra/logs/logs.go +++ b/internal/infra/logs/logs.go @@ -9,6 +9,7 @@ import ( "io" "os" "strings" + "sync" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" @@ -32,6 +33,11 @@ const ( bold = "\033[1m" ) +var ( + defaultLogger *Logger + once sync.Once +) + // Logger 是一个封装了 zap.SugaredLogger 的日志记录器。 // 它提供了结构化日志记录的各种方法,并实现了 io.Writer 接口以兼容 Gin。 type Logger struct { @@ -64,10 +70,51 @@ func NewLogger(cfg config.LogConfig) *Logger { return &Logger{zapLogger.Sugar()} } +// InitDefaultLogger 初始化包级单例的 defaultLogger。 +// 此函数应在应用程序启动时调用一次。 +func InitDefaultLogger(cfg config.LogConfig) { + once.Do(func() { + defaultLogger = NewLogger(cfg) + }) +} + +// GetLogger 从 Context 中提取调用链信息,并返回一个携带这些信息的 Logger 副本。 +// 如果 Context 中没有调用链信息,则直接返回默认的 Logger。 +func GetLogger(ctx context.Context) *Logger { + if defaultLogger == nil { + // 在调用 InitDefaultLogger 之前,提供一个备用的、仅输出到控制台的 Logger + fallbackCfg := config.LogConfig{Level: "info", Format: "console"} + return NewLogger(fallbackCfg) + } + + val := ctx.Value(chainKey) + if val == nil { + return defaultLogger + } + + chain, ok := val.([]string) + if !ok || len(chain) == 0 { + return defaultLogger + } + + // 使用 With 方法创建带有 "trace" 字段的 Logger 副本 + newSugaredLogger := defaultLogger.With("trace", strings.Join(chain, "->")) + return &Logger{newSugaredLogger} +} + +// Trace 是构建和记录调用链的核心函数。 +// 它首先使用 AddFuncName 创建一个包含新调用节点的新 Context, +// 然后返回这个新 Context 和一个包含完整调用链的 Logger 实例。 +func Trace(upstreamCtx context.Context, selfCtx context.Context, funcName string) (context.Context, *Logger) { + newCtx := AddFuncName(upstreamCtx, selfCtx, funcName) + logger := GetLogger(newCtx) + return newCtx, logger +} + // GetEncoder 根据指定的格式返回一个 zapcore.Encoder。 func GetEncoder(format string) zapcore.Encoder { encoderConfig := zap.NewProductionEncoderConfig() - encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 时间格式: 2006-01-02T15:04:05.000Z0700 + encoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder // 时间格式: 2006-01-02T15:04:05Z07:00 encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 日志级别大写: INFO (由 coloredConsoleEncoder 处理颜色) if format == "json" { @@ -190,17 +237,17 @@ func (g *GormLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface { // Info 打印 Info 级别的日志。 func (g *GormLogger) Info(ctx context.Context, msg string, data ...interface{}) { - g.ZapLogger.Infof(msg, data...) + GetLogger(ctx).Infof(msg, data...) } // Warn 打印 Warn 级别的日志。 func (g *GormLogger) Warn(ctx context.Context, msg string, data ...interface{}) { - g.ZapLogger.Warnf(msg, data...) + GetLogger(ctx).Warnf(msg, data...) } // Error 打印 Error 级别的日志。 func (g *GormLogger) Error(ctx context.Context, msg string, data ...interface{}) { - g.ZapLogger.Errorf(msg, data...) + GetLogger(ctx).Errorf(msg, data...) } // Trace 打印 SQL 查询日志,这是 GORM 日志的核心。 @@ -214,25 +261,27 @@ func (g *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql "elapsed", fmt.Sprintf("%.3fms", float64(elapsed.Nanoseconds())/1e6), } - // --- 逻辑修复开始 --- + // 获取带有调用链的 logger + logger := GetLogger(ctx) + 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) + logger.With(fields...).Errorf("[GORM] error: %s", err) return } // 如果查询时间超过慢查询阈值,则记录警告 if g.SlowThreshold != 0 && elapsed > g.SlowThreshold { - g.ZapLogger.With(fields...).Warnf("[GORM] slow query") + logger.With(fields...).Warnf("[GORM] slow query") return } // 正常情况,记录 Debug 级别的 SQL 查询 - g.ZapLogger.With(fields...).Debugf("[GORM] trace") + logger.With(fields...).Debugf("[GORM] trace") // --- 逻辑修复结束 --- }