// logger/runtime_logger.go package logger import ( "encoding/json" "log" "os" "strings" "sync" "time" "git.x2erp.com/qdy/go-base/config/subconfigs" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) var ( runtimeLogger *zap.Logger runtimeSugared *zap.SugaredLogger serviceName string initOnce sync.Once ) // LogContext 日志上下文 type LogContext struct { TraceID string UserID string TenantID string InstanceName string } // ESWriter ES写入器 type ESWriter struct { serviceName string esURL string } func NewESWriter(serviceName, esURL string) *ESWriter { return &ESWriter{ serviceName: serviceName, esURL: esURL, } } func (w *ESWriter) Write(p []byte) (n int, err error) { // 解析日志数据 var logEntry map[string]interface{} if err := json.Unmarshal(p, &logEntry); err != nil { // 使用标准log输出到启动日志 log.Printf("[ES-WRITER] 解析日志JSON失败: %v", err) return len(p), nil // 返回成功,不阻塞主流程 } // 动态生成索引名:service-日期 indexName := strings.ToLower(w.serviceName) + "-" + time.Now().Format("2006-01-02") // 使用标准log输出到启动日志 log.Printf("[ES-WRITER] 开始写入ES日志") log.Printf("索引名称: %s", indexName) log.Printf("数据长度: %d 字节", len(p)) log.Printf("服务名称: %s", w.serviceName) log.Printf("写入时间: %s", time.Now().Format("2006-01-02 15:04:05")) // TODO: 这里是实际写入ES的代码 // 这里只是示例,实际应该连接ES并写入数据 // esClient.Index().Index(indexName).BodyJson(logEntry).Do(ctx) // 模拟写入ES(在开发环境可以打印到控制台查看) w.simulateESWrite(indexName, logEntry) return len(p), nil } // simulateESWrite 模拟ES写入(仅用于开发调试) func (w *ESWriter) simulateESWrite(indexName string, data map[string]interface{}) { // 在开发环境,可以打印到控制台查看 if os.Getenv("GO_ENV") == "development" { jsonData, _ := json.MarshalIndent(data, "", " ") log.Printf("[ES-SIMULATE] === ES写入模拟开始 ===") log.Printf("索引名称: %s", indexName) log.Printf("写入时间: %s", time.Now().Format("2006-01-02 15:04:05")) log.Printf("jsonData: %s", string(jsonData)) //log.Printf("[ES-SIMULATE] 索引: %s\n数据:\n%s", indexName, string(jsonData)) } } func (w *ESWriter) Sync() error { // ES客户端通常不需要同步 log.Printf("[ES-WRITER] 同步ES写入器") return nil } // InitRuntimeLogger 初始化 func InitRuntimeLogger(svcName string, logConfig *subconfigs.LogConfig) { initOnce.Do(func() { if logConfig == nil { log.Fatal("错误: 日志配置不能为空") } //serviceName = svcName // 创建logger if err := createRuntimeLogger(svcName, logConfig); err != nil { log.Fatal("创建运行时日志器失败: ", err) } log.Printf("[RUNTIME-LOGGER] 运行时日志系统初始化完成") log.Printf("服务名称: %s", svcName) log.Printf("日志级别: %s", logConfig.Level) }) } func createRuntimeLogger(svcName 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 if cfg.JSONFormat { // JSON格式输出 consoleEncoder = zapcore.NewJSONEncoder(encoderConfig) } else { // 文本格式输出 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, }) } jsonEncoder = zapcore.NewJSONEncoder(encoderConfig) // 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] 初始化控制台日志输出(文本格式)") 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": // ES输出 log.Printf("[RUNTIME-LOGGER] 初始化ES日志输出, 路径: %s", cfg.ESPath) esWriter := NewESWriter(svcName, cfg.ESPath) esCore := zapcore.NewCore( jsonEncoder, zapcore.AddSync(esWriter), level, ) cores = append(cores, esCore) } } // 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), ), ) 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 LogContext) *zap.SugaredLogger { // 直接使用,不会为空 return runtimeLogger.With( zap.String("service", serviceName), 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 LogContext, format string, args ...interface{}) { W(ctx).Infof(format, args...) } // ErrorC 带上下文的Error日志 func ErrorC(ctx LogContext, format string, args ...interface{}) { W(ctx).Errorf(format, args...) } // DebugC 带上下文的Debug日志 func DebugC(ctx LogContext, format string, args ...interface{}) { W(ctx).Debugf(format, args...) } // WarnC 带上下文的Warn日志 func WarnC(ctx LogContext, format string, args ...interface{}) { W(ctx).Warnf(format, args...) } // FatalC 带上下文的Fatal日志 func FatalC(ctx LogContext, 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...) } // ============ 其他方法 ============ // GetZapLogger 获取原始zap.Logger func GetZapLogger() *zap.Logger { return runtimeLogger } // Sync 同步日志 func Sync() error { if runtimeLogger != nil { return runtimeLogger.Sync() } return nil }