// logger/runtime_logger.go package logger import ( "fmt" "log" "os" "strings" "sync" "time" "git.x2erp.com/qdy/go-base/config" "git.x2erp.com/qdy/go-base/config/subconfigs" "git.x2erp.com/qdy/go-base/ctx" "git.x2erp.com/qdy/go-base/logger/http" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) type RunTimeLoggerFactory struct { } var ( runtimeLogger *zap.Logger runtimeSugared *zap.SugaredLogger serviceName string serviceVersion string initOnce sync.Once ) // // LogContext 日志上下文 // type LogContext struct { // TraceID string // UserID string // TenantID string // InstanceName string // } // InitRuntimeLogger 初始化 func InitRuntimeLogger(cfg config.IConfig) *RunTimeLoggerFactory { svcName := cfg.GetAppName() svcVersion := cfg.GetAppVersion() logConfig := cfg.GetLogConfig() initOnce.Do(func() { if logConfig == nil { log.Fatal("错误: 日志配置不能为空") } serviceName = svcName serviceVersion = svcVersion // 创建logger if err := createRuntimeLogger(svcName, svcVersion, logConfig); err != nil { log.Fatal("创建运行时日志器失败: ", err) } log.Printf("[RUNTIME-LOGGER] 运行时日志系统初始化完成") log.Printf("服务名称: %s", svcName) log.Printf("日志级别: %s", logConfig.Level) }) return &RunTimeLoggerFactory{} } func createRuntimeLogger(svcName, svcVersion string, cfg *subconfigs.LogConfig) error { // 1. 设置日志级别 level := zap.InfoLevel switch strings.ToLower(cfg.Level) { case "debug": level = zap.DebugLevel case "warn": level = zap.WarnLevel case "error": level = zap.ErrorLevel } // 2. 编码器配置 encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder encoderConfig.TimeKey = "timestamp" encoderConfig.MessageKey = "message" encoderConfig.LevelKey = "level" encoderConfig.CallerKey = "caller" // 3. 根据JSONFormat决定编码器 var consoleEncoder, jsonEncoder zapcore.Encoder // JSON格式输出 jsonEncoder = zapcore.NewJSONEncoder(encoderConfig) // 文本格式输出 consoleEncoder = zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ TimeKey: "T", LevelKey: "L", NameKey: "N", CallerKey: "C", MessageKey: "M", StacktraceKey: "S", LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalColorLevelEncoder, EncodeTime: zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05"), EncodeDuration: zapcore.StringDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, EncodeName: nil, }) // 4. 输出目标 cores := []zapcore.Core{} // 4.1 根据Output字段决定输出目标 outputs := strings.Split(cfg.Output, ",") for _, output := range outputs { output = strings.TrimSpace(strings.ToLower(output)) switch output { case "console", "stdout": // 控制台输出 consoleCore := zapcore.NewCore( consoleEncoder, zapcore.AddSync(os.Stdout), level, ) cores = append(cores, consoleCore) log.Printf("[RUNTIME-LOGGER] 初始化控制台日志输出(文本格式)%s", svcVersion) case "file": // 文件输出(JSON格式) if cfg.FilePath != "" { // 处理文件路径,确保有日期 filePath := cfg.FilePath datedFilePath := addDateToFilePath(filePath) log.Printf("[RUNTIME-LOGGER] 创建运行时日志文件: %s", datedFilePath) fileWriter := zapcore.AddSync(&lumberjack.Logger{ Filename: datedFilePath, MaxSize: cfg.MaxSize, // MB MaxBackups: cfg.MaxBackups, // 保留的文件数 MaxAge: cfg.MaxAge, // 天数 Compress: cfg.Compress, }) fileCore := zapcore.NewCore( jsonEncoder, fileWriter, level, ) cores = append(cores, fileCore) } case "es": // 1. 检查必要配置 if cfg.ESPath == "" { log.Printf("[RUNTIME-LOGGER] ES地址未配置,跳过ES输出") break } // 判断是否包含console输出 containsConsole := strings.Contains(strings.ToLower(cfg.Output), "console") // 2. 初始化全局ES写入器 err := http.InitESWriter( svcName, cfg.ESPath, cfg.ESUsername, cfg.ESPassword, containsConsole, ) if err != nil { log.Printf("[RUNTIME-LOGGER] ES写入器初始化失败: %v", err) break } // 3. 创建zap core esCore := zapcore.NewCore( jsonEncoder, zapcore.AddSync(http.GetESWriter()), // 使用全局实例 level, ) cores = append(cores, esCore) log.Printf("[RUNTIME-LOGGER] ES日志输出已启用: %s", cfg.ESPath) } } // 5. 如果Output为空,默认输出到控制台 if len(cores) == 0 { consoleCore := zapcore.NewCore( consoleEncoder, zapcore.AddSync(os.Stdout), level, ) cores = append(cores, consoleCore) } // 6. 创建组合core core := zapcore.NewTee(cores...) // 7. 创建logger runtimeLogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zap.ErrorLevel), //zap.Fields( // zap.String("service", svcName), // zap.String("version", serviceVersion), //), ) runtimeSugared = runtimeLogger.Sugar() return nil } // addDateToFilePath 在文件路径中添加日期 func addDateToFilePath(filePath string) string { // 如果已经包含 %s,则替换为日期 if strings.Contains(filePath, "%s") { return strings.ReplaceAll(filePath, "%s", time.Now().Format("20060102")) } // 如果没有 %s,在扩展名前添加日期 extIndex := strings.LastIndex(filePath, ".") if extIndex > 0 { return filePath[:extIndex] + "-" + time.Now().Format("20060102") + filePath[extIndex:] } // 没有扩展名,直接在末尾添加日期 return filePath + "-" + time.Now().Format("20060102") } // WithC 创建带有上下文的logger(C代表Context) func W(ctx *ctx.RequestContext) *zap.SugaredLogger { // 直接使用,不会为空 return runtimeLogger.With( zap.String("service", serviceName), zap.String("version", serviceVersion), zap.String("trace_id", ctx.TraceID), zap.String("user_id", ctx.UserID), zap.String("tenant_id", ctx.TenantID), zap.String("instance_name", ctx.InstanceName), ).Sugar() } // ============ 上下文日志快捷方法 ============ // InfoC 带上下文的Info日志 func InfoC(ctx *ctx.RequestContext, format string, args ...interface{}) { W(ctx).Infof(format, args...) } // ErrorC 带上下文的Error日志 func ErrorC(ctx *ctx.RequestContext, format string, args ...interface{}) { W(ctx).Errorf(format, args...) } // DebugC 带上下文的Debug日志 func DebugC(ctx *ctx.RequestContext, format string, args ...interface{}) { W(ctx).Debugf(format, args...) } // WarnC 带上下文的Warn日志 func WarnC(ctx *ctx.RequestContext, format string, args ...interface{}) { W(ctx).Warnf(format, args...) } // FatalC 带上下文的Fatal日志 func FatalC(ctx *ctx.RequestContext, format string, args ...interface{}) { W(ctx).Fatalf(format, args...) } // ============ 原始日志方法 ============ // Info 普通Info日志 func Info(format string, args ...interface{}) { runtimeSugared.Infof(format, args...) } // Error 普通Error日志 func Error(format string, args ...interface{}) { runtimeSugared.Errorf(format, args...) } // Debug 普通Debug日志 func Debug(format string, args ...interface{}) { runtimeSugared.Debugf(format, args...) } // Warn 普通Warn日志 func Warn(format string, args ...interface{}) { runtimeSugared.Warnf(format, args...) } // Fatal 普通Fatal日志 func Fatal(format string, args ...interface{}) { runtimeSugared.Fatalf(format, args...) } // Errorf 记录错误日志并返回格式化错误 func Errorf(format string, args ...interface{}) error { err := fmt.Errorf(format, args...) runtimeSugared.Errorf(format, args...) return err } // ErrorCf 带上下文的错误日志并返回格式化错误 func ErrorCf(ctx *ctx.RequestContext, format string, args ...interface{}) error { err := fmt.Errorf(format, args...) ErrorC(ctx, format, args...) return err } // ============ 其他方法 ============ // GetZapLogger 获取原始zap.Logger func GetZapLogger() *zap.Logger { return runtimeLogger } // Sync 同步日志 func Sync() error { if runtimeLogger != nil { return runtimeLogger.Sync() } return nil } // StopESWriter 停止ES写入器 func StopESWriter() { http.StopESWriter() } // IsDebug 判断是否启用Debug级别 func IsDebug() bool { if runtimeLogger == nil { return false } return runtimeLogger.Core().Enabled(zapcore.DebugLevel) } func (f *RunTimeLoggerFactory) GetName() string { return "RunTimeLoggerFactory" } // RunTimeLoggerFactory 关闭启动日志,切换到运行日志 func (f *RunTimeLoggerFactory) Close() { // 1. 同步日志缓冲区 if runtimeLogger != nil { _ = runtimeLogger.Sync() // 忽略错误,因为sync可能失败 } // 2. 关闭ES写入器(如果有) StopESWriter() // 3. 清理资源 // 注意:zap.Logger 不需要显式关闭,Sync() 已足够 // 5. 使用标准库日志记录关闭信息 if serviceName != "" { log.Printf("[RUNTIME-LOGGER] 服务 %s 日志系统已关闭", serviceName) } else { log.Printf("[RUNTIME-LOGGER] 日志系统已关闭") } }