316 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package external_logger
 | ||
| 
 | ||
| import (
 | ||
| 	"fmt"
 | ||
| 	"os"
 | ||
| 	"path/filepath"
 | ||
| 	"time"
 | ||
| 
 | ||
| 	"go.uber.org/zap"
 | ||
| 	"go.uber.org/zap/zapcore"
 | ||
| 	"gopkg.in/natefinch/lumberjack.v2"
 | ||
| )
 | ||
| 
 | ||
| // ExternalServiceLoggingConfig 外部服务日志配置
 | ||
| type ExternalServiceLoggingConfig struct {
 | ||
| 	Enabled               bool                                      `yaml:"enabled"`
 | ||
| 	LogDir                string                                    `yaml:"log_dir"`
 | ||
| 	ServiceName           string                                    `yaml:"service_name"` // 服务名称,用于区分日志目录
 | ||
| 	UseDaily              bool                                      `yaml:"use_daily"`
 | ||
| 	EnableLevelSeparation bool                                      `yaml:"enable_level_separation"`
 | ||
| 	LevelConfigs          map[string]ExternalServiceLevelFileConfig `yaml:"level_configs"`
 | ||
| }
 | ||
| 
 | ||
| // ExternalServiceLevelFileConfig 外部服务级别文件配置
 | ||
| type ExternalServiceLevelFileConfig struct {
 | ||
| 	MaxSize    int  `yaml:"max_size"`
 | ||
| 	MaxBackups int  `yaml:"max_backups"`
 | ||
| 	MaxAge     int  `yaml:"max_age"`
 | ||
| 	Compress   bool `yaml:"compress"`
 | ||
| }
 | ||
| 
 | ||
| // ExternalServiceLogger 外部服务日志器
 | ||
| type ExternalServiceLogger struct {
 | ||
| 	logger      *zap.Logger
 | ||
| 	config      ExternalServiceLoggingConfig
 | ||
| 	serviceName string
 | ||
| }
 | ||
| 
 | ||
| // NewExternalServiceLogger 创建外部服务日志器
 | ||
| func NewExternalServiceLogger(config ExternalServiceLoggingConfig) (*ExternalServiceLogger, error) {
 | ||
| 	if !config.Enabled {
 | ||
| 		return &ExternalServiceLogger{
 | ||
| 			logger:      zap.NewNop(),
 | ||
| 			serviceName: config.ServiceName,
 | ||
| 		}, nil
 | ||
| 	}
 | ||
| 
 | ||
| 	// 根据服务名称创建独立的日志目录
 | ||
| 	serviceLogDir := filepath.Join(config.LogDir, config.ServiceName)
 | ||
| 
 | ||
| 	// 确保日志目录存在
 | ||
| 	if err := os.MkdirAll(serviceLogDir, 0755); err != nil {
 | ||
| 		return nil, fmt.Errorf("创建日志目录失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 创建基础配置
 | ||
| 	zapConfig := zap.NewProductionConfig()
 | ||
| 	zapConfig.OutputPaths = []string{"stdout"}
 | ||
| 	zapConfig.ErrorOutputPaths = []string{"stderr"}
 | ||
| 
 | ||
| 	// 创建基础logger
 | ||
| 	baseLogger, err := zapConfig.Build()
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("创建基础logger失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 如果启用级别分离,创建文件输出
 | ||
| 	if config.EnableLevelSeparation {
 | ||
| 		core := createSeparatedCore(serviceLogDir, config)
 | ||
| 		baseLogger = zap.New(core)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 创建日志器实例
 | ||
| 	logger := &ExternalServiceLogger{
 | ||
| 		logger:      baseLogger,
 | ||
| 		config:      config,
 | ||
| 		serviceName: config.ServiceName,
 | ||
| 	}
 | ||
| 
 | ||
| 	// 如果启用按天分隔,启动定时清理任务
 | ||
| 	if config.UseDaily {
 | ||
| 		go logger.startCleanupTask()
 | ||
| 	}
 | ||
| 
 | ||
| 	return logger, nil
 | ||
| }
 | ||
| 
 | ||
| // createSeparatedCore 创建分离的日志核心
 | ||
| func createSeparatedCore(logDir string, config ExternalServiceLoggingConfig) zapcore.Core {
 | ||
| 	// 创建编码器
 | ||
| 	encoderConfig := zap.NewProductionEncoderConfig()
 | ||
| 	encoderConfig.TimeKey = "timestamp"
 | ||
| 	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
 | ||
| 	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
 | ||
| 
 | ||
| 	// 创建不同级别的文件输出
 | ||
| 	infoWriter := createFileWriter(logDir, "info", config.LevelConfigs["info"], config.ServiceName, config.UseDaily)
 | ||
| 	errorWriter := createFileWriter(logDir, "error", config.LevelConfigs["error"], config.ServiceName, config.UseDaily)
 | ||
| 	warnWriter := createFileWriter(logDir, "warn", config.LevelConfigs["warn"], config.ServiceName, config.UseDaily)
 | ||
| 
 | ||
| 	// 修复:创建真正的级别分离核心
 | ||
| 	// 使用自定义的LevelEnabler来确保每个Core只处理特定级别的日志
 | ||
| 	infoCore := zapcore.NewCore(
 | ||
| 		zapcore.NewJSONEncoder(encoderConfig),
 | ||
| 		zapcore.AddSync(infoWriter),
 | ||
| 		&levelEnabler{minLevel: zapcore.InfoLevel, maxLevel: zapcore.InfoLevel}, // 只接受INFO级别
 | ||
| 	)
 | ||
| 
 | ||
| 	errorCore := zapcore.NewCore(
 | ||
| 		zapcore.NewJSONEncoder(encoderConfig),
 | ||
| 		zapcore.AddSync(errorWriter),
 | ||
| 		&levelEnabler{minLevel: zapcore.ErrorLevel, maxLevel: zapcore.ErrorLevel}, // 只接受ERROR级别
 | ||
| 	)
 | ||
| 
 | ||
| 	warnCore := zapcore.NewCore(
 | ||
| 		zapcore.NewJSONEncoder(encoderConfig),
 | ||
| 		zapcore.AddSync(warnWriter),
 | ||
| 		&levelEnabler{minLevel: zapcore.WarnLevel, maxLevel: zapcore.WarnLevel}, // 只接受WARN级别
 | ||
| 	)
 | ||
| 
 | ||
| 	// 使用 zapcore.NewTee 合并核心,现在每个核心只会处理自己级别的日志
 | ||
| 	return zapcore.NewTee(infoCore, errorCore, warnCore)
 | ||
| }
 | ||
| 
 | ||
| // levelEnabler 自定义级别过滤器,确保只接受指定级别的日志
 | ||
| type levelEnabler struct {
 | ||
| 	minLevel zapcore.Level
 | ||
| 	maxLevel zapcore.Level
 | ||
| }
 | ||
| 
 | ||
| // Enabled 实现 zapcore.LevelEnabler 接口
 | ||
| func (l *levelEnabler) Enabled(level zapcore.Level) bool {
 | ||
| 	return level >= l.minLevel && level <= l.maxLevel
 | ||
| }
 | ||
| 
 | ||
| // createFileWriter 创建文件写入器
 | ||
| func createFileWriter(logDir, level string, config ExternalServiceLevelFileConfig, serviceName string, useDaily bool) *lumberjack.Logger {
 | ||
| 	// 使用默认配置如果未指定
 | ||
| 	if config.MaxSize == 0 {
 | ||
| 		config.MaxSize = 100
 | ||
| 	}
 | ||
| 	if config.MaxBackups == 0 {
 | ||
| 		config.MaxBackups = 3
 | ||
| 	}
 | ||
| 	if config.MaxAge == 0 {
 | ||
| 		config.MaxAge = 28
 | ||
| 	}
 | ||
| 
 | ||
| 	// 构建文件名
 | ||
| 	var filename string
 | ||
| 	if useDaily {
 | ||
| 		// 按天分隔:logs/westdex/2024-01-01/westdex_info.log
 | ||
| 		date := time.Now().Format("2006-01-02")
 | ||
| 		dateDir := filepath.Join(logDir, date)
 | ||
| 		
 | ||
| 		// 确保日期目录存在
 | ||
| 		if err := os.MkdirAll(dateDir, 0755); err != nil {
 | ||
| 			// 如果创建日期目录失败,回退到根目录
 | ||
| 			filename = filepath.Join(logDir, fmt.Sprintf("%s_%s.log", serviceName, level))
 | ||
| 		} else {
 | ||
| 			filename = filepath.Join(dateDir, fmt.Sprintf("%s_%s.log", serviceName, level))
 | ||
| 		}
 | ||
| 	} else {
 | ||
| 		// 传统方式:logs/westdex/westdex_info.log
 | ||
| 		filename = filepath.Join(logDir, fmt.Sprintf("%s_%s.log", serviceName, level))
 | ||
| 	}
 | ||
| 
 | ||
| 	return &lumberjack.Logger{
 | ||
| 		Filename:   filename,
 | ||
| 		MaxSize:    config.MaxSize,
 | ||
| 		MaxBackups: config.MaxBackups,
 | ||
| 		MaxAge:     config.MaxAge,
 | ||
| 		Compress:   config.Compress,
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // LogRequest 记录请求日志
 | ||
| func (e *ExternalServiceLogger) LogRequest(requestID, apiCode string, url interface{}, params interface{}) {
 | ||
| 	e.logger.Info(fmt.Sprintf("%s API请求", e.serviceName),
 | ||
| 		zap.String("service", e.serviceName),
 | ||
| 		zap.String("request_id", requestID),
 | ||
| 		zap.String("api_code", apiCode),
 | ||
| 		zap.Any("url", url),
 | ||
| 		zap.Any("params", params),
 | ||
| 		zap.String("timestamp", time.Now().Format(time.RFC3339)),
 | ||
| 	)
 | ||
| }
 | ||
| 
 | ||
| // LogResponse 记录响应日志
 | ||
| func (e *ExternalServiceLogger) LogResponse(requestID, apiCode string, statusCode int, response []byte, duration time.Duration) {
 | ||
| 	e.logger.Info(fmt.Sprintf("%s API响应", e.serviceName),
 | ||
| 		zap.String("service", e.serviceName),
 | ||
| 		zap.String("request_id", requestID),
 | ||
| 		zap.String("api_code", apiCode),
 | ||
| 		zap.Int("status_code", statusCode),
 | ||
| 		zap.String("response", string(response)),
 | ||
| 		zap.Duration("duration", duration),
 | ||
| 		zap.String("timestamp", time.Now().Format(time.RFC3339)),
 | ||
| 	)
 | ||
| }
 | ||
| 
 | ||
| // LogError 记录错误日志
 | ||
| func (e *ExternalServiceLogger) LogError(requestID, apiCode string, err error, params interface{}) {
 | ||
| 	e.logger.Error(fmt.Sprintf("%s API错误", e.serviceName),
 | ||
| 		zap.String("service", e.serviceName),
 | ||
| 		zap.String("request_id", requestID),
 | ||
| 		zap.String("api_code", apiCode),
 | ||
| 		zap.Error(err),
 | ||
| 		zap.Any("params", params),
 | ||
| 		zap.String("timestamp", time.Now().Format(time.RFC3339)),
 | ||
| 	)
 | ||
| }
 | ||
| 
 | ||
| // LogInfo 记录信息日志
 | ||
| func (e *ExternalServiceLogger) LogInfo(message string, fields ...zap.Field) {
 | ||
| 	allFields := []zap.Field{zap.String("service", e.serviceName)}
 | ||
| 	allFields = append(allFields, fields...)
 | ||
| 	e.logger.Info(message, allFields...)
 | ||
| }
 | ||
| 
 | ||
| // LogWarn 记录警告日志
 | ||
| func (e *ExternalServiceLogger) LogWarn(message string, fields ...zap.Field) {
 | ||
| 	allFields := []zap.Field{zap.String("service", e.serviceName)}
 | ||
| 	allFields = append(allFields, fields...)
 | ||
| 	e.logger.Warn(message, allFields...)
 | ||
| }
 | ||
| 
 | ||
| // LogErrorWithFields 记录带字段的错误日志
 | ||
| func (e *ExternalServiceLogger) LogErrorWithFields(message string, fields ...zap.Field) {
 | ||
| 	allFields := []zap.Field{zap.String("service", e.serviceName)}
 | ||
| 	allFields = append(allFields, fields...)
 | ||
| 	e.logger.Error(message, allFields...)
 | ||
| }
 | ||
| 
 | ||
| // Sync 同步日志
 | ||
| func (e *ExternalServiceLogger) Sync() error {
 | ||
| 	return e.logger.Sync()
 | ||
| }
 | ||
| 
 | ||
| // CleanupOldDateDirs 清理过期的日期目录
 | ||
| func (e *ExternalServiceLogger) CleanupOldDateDirs() error {
 | ||
| 	if !e.config.UseDaily {
 | ||
| 		return nil
 | ||
| 	}
 | ||
| 
 | ||
| 	logDir := filepath.Join(e.config.LogDir, e.serviceName)
 | ||
| 	
 | ||
| 	// 读取日志目录
 | ||
| 	entries, err := os.ReadDir(logDir)
 | ||
| 	if err != nil {
 | ||
| 		return fmt.Errorf("读取日志目录失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 计算过期时间(基于配置的MaxAge)
 | ||
| 	maxAge := 28 // 默认28天
 | ||
| 	if errorConfig, exists := e.config.LevelConfigs["error"]; exists && errorConfig.MaxAge > 0 {
 | ||
| 		maxAge = errorConfig.MaxAge
 | ||
| 	}
 | ||
| 	
 | ||
| 	cutoffTime := time.Now().AddDate(0, 0, -maxAge)
 | ||
| 
 | ||
| 	for _, entry := range entries {
 | ||
| 		if !entry.IsDir() {
 | ||
| 			continue
 | ||
| 		}
 | ||
| 
 | ||
| 		// 尝试解析目录名为日期
 | ||
| 		dirName := entry.Name()
 | ||
| 		dirTime, err := time.Parse("2006-01-02", dirName)
 | ||
| 		if err != nil {
 | ||
| 			// 如果不是日期格式的目录,跳过
 | ||
| 			continue
 | ||
| 		}
 | ||
| 
 | ||
| 		// 检查是否过期
 | ||
| 		if dirTime.Before(cutoffTime) {
 | ||
| 			dirPath := filepath.Join(logDir, dirName)
 | ||
| 			if err := os.RemoveAll(dirPath); err != nil {
 | ||
| 				return fmt.Errorf("删除过期目录失败 %s: %w", dirPath, err)
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	return nil
 | ||
| }
 | ||
| 
 | ||
| // startCleanupTask 启动定时清理任务
 | ||
| func (e *ExternalServiceLogger) startCleanupTask() {
 | ||
| 	// 每天凌晨2点执行清理
 | ||
| 	ticker := time.NewTicker(24 * time.Hour)
 | ||
| 	defer ticker.Stop()
 | ||
| 
 | ||
| 	// 等待到下一个凌晨2点
 | ||
| 	now := time.Now()
 | ||
| 	next := time.Date(now.Year(), now.Month(), now.Day()+1, 2, 0, 0, 0, now.Location())
 | ||
| 	time.Sleep(time.Until(next))
 | ||
| 
 | ||
| 	// 立即执行一次清理
 | ||
| 	if err := e.CleanupOldDateDirs(); err != nil {
 | ||
| 		// 记录清理错误(这里使用标准输出,因为logger可能还未初始化)
 | ||
| 		fmt.Printf("清理过期日志目录失败: %v\n", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 定时执行清理
 | ||
| 	for range ticker.C {
 | ||
| 		if err := e.CleanupOldDateDirs(); err != nil {
 | ||
| 			fmt.Printf("清理过期日志目录失败: %v\n", err)
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // GetServiceName 获取服务名称
 | ||
| func (e *ExternalServiceLogger) GetServiceName() string {
 | ||
| 	return e.serviceName
 | ||
| }
 |