diff --git a/configs/env.production.yaml b/configs/env.production.yaml index 16b366e..d0d0eda 100644 --- a/configs/env.production.yaml +++ b/configs/env.production.yaml @@ -47,6 +47,50 @@ logger: max_age: 30 compress: true use_daily: true + # 启用按级别分文件 + enable_level_separation: true + # 各级别日志文件配置 + level_configs: + debug: + max_size: 50 # 50MB + max_backups: 3 + max_age: 7 # 7天 + compress: true + info: + max_size: 100 # 100MB + max_backups: 5 + max_age: 30 # 30天 + compress: true + warn: + max_size: 100 # 100MB + max_backups: 5 + max_age: 30 # 30天 + compress: true + error: + max_size: 200 # 200MB + max_backups: 10 + max_age: 90 # 90天 + compress: true + fatal: + max_size: 100 # 100MB + max_backups: 10 + max_age: 365 # 1年 + compress: true + panic: + max_size: 100 # 100MB + max_backups: 10 + max_age: 365 # 1年 + compress: true + # 生产环境全面日志配置 + comprehensive_logging: + enable_request_logging: true + enable_response_logging: true + enable_request_body_logging: false # 生产环境不记录请求体(安全考虑) + enable_error_logging: true + enable_business_logging: true + enable_performance_logging: true + max_body_size: 10240 # 10KB + exclude_paths: ["/health", "/metrics", "/favicon.ico", "/swagger"] # =========================================== # 🔐 JWT配置 # =========================================== diff --git a/docs/按级别分文件日志系统说明.md b/docs/按级别分文件日志系统说明.md new file mode 100644 index 0000000..31a7d8b --- /dev/null +++ b/docs/按级别分文件日志系统说明.md @@ -0,0 +1,344 @@ +# 📝 按级别分文件日志系统说明 + +## 概述 + +本系统支持按日志级别分文件存储,不同级别的日志写入不同的文件,便于日志管理和分析。 + +## 功能特性 + +### ✅ 按级别分文件 +- **Debug级别**: `debug.log` - 调试信息 +- **Info级别**: `info.log` - 一般信息 +- **Warn级别**: `warn.log` - 警告信息 +- **Error级别**: `error.log` - 错误信息 +- **Fatal级别**: `fatal.log` - 致命错误 +- **Panic级别**: `panic.log` - 恐慌错误 + +### ✅ 独立配置 +- 每个级别可以独立配置文件大小、备份数量、保留天数 +- 支持不同级别的压缩策略 +- 灵活的轮转配置 + +### ✅ 按日分包支持 +- 支持按日期分包:`logs/2024-01-01/error.log` +- 传统模式:`logs/error.log` + +## 配置说明 + +### 基础配置 + +```yaml +logger: + # 基础配置 + level: info + format: json + output: "file" + log_dir: "/app/logs" + use_daily: true + + # 启用按级别分文件 + enable_level_separation: true + + # 各级别配置 + level_configs: + debug: + max_size: 50 # 50MB + max_backups: 3 + max_age: 7 # 7天 + compress: true + info: + max_size: 100 # 100MB + max_backups: 5 + max_age: 30 # 30天 + compress: true + warn: + max_size: 100 # 100MB + max_backups: 5 + max_age: 30 # 30天 + compress: true + error: + max_size: 200 # 200MB + max_backups: 10 + max_age: 90 # 90天 + compress: true + fatal: + max_size: 100 # 100MB + max_backups: 10 + max_age: 365 # 1年 + compress: true + panic: + max_size: 100 # 100MB + max_backups: 10 + max_age: 365 # 1年 + compress: true +``` + +## 日志目录结构 + +### 按日分包模式 +``` +logs/ +├── 2024-01-01/ +│ ├── debug.log # 调试日志 +│ ├── debug.log.1 # 调试日志备份 +│ ├── info.log # 信息日志 +│ ├── info.log.1 # 信息日志备份 +│ ├── warn.log # 警告日志 +│ ├── warn.log.1 # 警告日志备份 +│ ├── error.log # 错误日志 +│ ├── error.log.1 # 错误日志备份 +│ ├── fatal.log # 致命错误日志 +│ └── panic.log # 恐慌错误日志 +├── 2024-01-02/ +│ ├── debug.log +│ ├── info.log +│ ├── warn.log +│ ├── error.log +│ ├── fatal.log +│ └── panic.log +``` + +### 传统模式 +``` +logs/ +├── debug.log # 调试日志 +├── debug.log.1 # 调试日志备份 +├── info.log # 信息日志 +├── info.log.1 # 信息日志备份 +├── warn.log # 警告日志 +├── warn.log.1 # 警告日志备份 +├── error.log # 错误日志 +├── error.log.1 # 错误日志备份 +├── fatal.log # 致命错误日志 +└── panic.log # 恐慌错误日志 +``` + +## 级别配置策略 + +### 1. Debug级别 +- **用途**: 详细的调试信息 +- **保留策略**: 短期保留(7天) +- **文件大小**: 较小(50MB) +- **备份数量**: 较少(3个) + +### 2. Info级别 +- **用途**: 一般业务信息 +- **保留策略**: 中期保留(30天) +- **文件大小**: 中等(100MB) +- **备份数量**: 中等(5个) + +### 3. Warn级别 +- **用途**: 警告信息 +- **保留策略**: 中期保留(30天) +- **文件大小**: 中等(100MB) +- **备份数量**: 中等(5个) + +### 4. Error级别 +- **用途**: 错误信息 +- **保留策略**: 长期保留(90天) +- **文件大小**: 较大(200MB) +- **备份数量**: 较多(10个) + +### 5. Fatal级别 +- **用途**: 致命错误 +- **保留策略**: 永久保留(1年) +- **文件大小**: 中等(100MB) +- **备份数量**: 较多(10个) + +### 6. Panic级别 +- **用途**: 恐慌错误 +- **保留策略**: 永久保留(1年) +- **文件大小**: 中等(100MB) +- **备份数量**: 较多(10个) + +## 使用方法 + +### 1. 基本使用 + +```go +import "tyapi-server/internal/shared/logger" + +// 获取日志器 +log := logger.GetLogger() + +// 不同级别的日志会自动写入对应文件 +log.Debug("调试信息", zap.String("component", "user_service")) +log.Info("用户登录成功", zap.String("user_id", "12345")) +log.Warn("用户多次登录失败", zap.String("phone", "138****8888")) +log.Error("数据库连接失败", zap.Error(err)) +log.Fatal("系统无法启动", zap.Error(err)) +log.Panic("严重错误", zap.Error(err)) +``` + +### 2. 带上下文的日志 + +```go +// 从Gin上下文获取日志器 +ctx := c.Request.Context() +log := logger.GetLogger().WithContext(ctx) + +log.Info("处理用户请求", + zap.String("action", "user_login"), + zap.String("ip", c.ClientIP()), +) +``` + +### 3. 结构化日志字段 + +```go +log.Info("业务操作", + zap.String("operation", "create_user"), + zap.String("user_id", user.ID), + zap.Int("age", user.Age), + zap.Float64("score", 95.5), + zap.Bool("is_active", true), + zap.Error(err), +) +``` + +## 日志分析 + +### 1. 查看特定级别日志 + +```bash +# 查看错误日志 +tail -f logs/2024-01-01/error.log + +# 查看警告日志 +tail -f logs/2024-01-01/warn.log + +# 查看信息日志 +tail -f logs/2024-01-01/info.log +``` + +### 2. 统计各级别日志数量 + +```bash +# 统计错误日志数量 +wc -l logs/2024-01-01/error.log + +# 统计警告日志数量 +wc -l logs/2024-01-01/warn.log + +# 统计信息日志数量 +wc -l logs/2024-01-01/info.log +``` + +### 3. 搜索特定内容 + +```bash +# 在错误日志中搜索特定错误 +grep "数据库连接失败" logs/2024-01-01/error.log + +# 在警告日志中搜索特定警告 +grep "用户多次登录失败" logs/2024-01-01/warn.log + +# 在信息日志中搜索特定操作 +grep "用户登录成功" logs/2024-01-01/info.log +``` + +### 4. 使用jq分析JSON日志 + +```bash +# 分析错误日志 +cat logs/2024-01-01/error.log | jq 'select(.level == "error")' + +# 统计错误类型 +cat logs/2024-01-01/error.log | jq -r '.message' | sort | uniq -c + +# 查看特定用户的错误 +cat logs/2024-01-01/error.log | jq 'select(.user_id == "12345")' +``` + +## 监控和告警 + +### 1. 错误日志监控 + +```bash +# 监控错误日志增长 +watch -n 5 'wc -l logs/$(date +%Y-%m-%d)/error.log' + +# 监控错误日志大小 +watch -n 5 'ls -lh logs/$(date +%Y-%m-%d)/error.log' +``` + +### 2. 告警配置 + +```bash +# 检查错误日志是否超过阈值 +if [ $(wc -l < logs/$(date +%Y-%m-%d)/error.log) -gt 1000 ]; then + echo "错误日志数量过多,请检查系统状态" +fi + +# 检查错误日志文件大小 +if [ $(stat -c%s logs/$(date +%Y-%m-%d)/error.log) -gt 104857600 ]; then + echo "错误日志文件过大,请检查系统状态" +fi +``` + +## 性能优化 + +### 1. 异步写入 +- 日志写入是异步的,不会阻塞业务逻辑 +- 使用缓冲写入减少I/O操作 + +### 2. 级别过滤 +- 只记录配置的级别及以上级别的日志 +- 减少不必要的日志输出 + +### 3. 文件轮转 +- 自动轮转避免单个文件过大 +- 压缩旧日志文件节省磁盘空间 + +## 最佳实践 + +### 1. 级别使用规范 + +- **Debug**: 详细的调试信息(开发环境) +- **Info**: 重要的业务信息(生产环境) +- **Warn**: 需要关注但不影响主功能的问题 +- **Error**: 影响功能的错误信息 +- **Fatal**: 系统无法继续运行的错误 +- **Panic**: 程序崩溃的错误 + +### 2. 日志内容规范 + +- 记录关键业务操作 +- 包含足够的上下文信息 +- 使用结构化的字段 +- 避免记录敏感信息 + +### 3. 文件管理规范 + +- 定期清理旧日志文件 +- 监控日志文件大小 +- 设置合理的轮转策略 +- 备份重要日志 + +## 故障排除 + +### 1. 常见问题 + +**问题**: 某个级别的日志文件没有创建 +**解决**: 检查该级别是否在配置中定义 + +**问题**: 日志文件权限问题 +**解决**: 确保应用有写入日志目录的权限 + +**问题**: 磁盘空间不足 +**解决**: 调整日志保留策略,减少保留天数 + +### 2. 性能问题 + +**问题**: 日志写入影响性能 +**解决**: 检查是否启用了异步写入 + +**问题**: 日志文件过大 +**解决**: 调整文件大小限制和轮转策略 + +## 相关文件 + +- `internal/shared/logger/level_logger.go` - 按级别分文件日志器实现 +- `internal/config/config.go` - 日志配置结构 +- `configs/env.production.yaml` - 生产环境日志配置 +- `internal/container/container.go` - 依赖注入配置 \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index d90449f..b25244e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -90,6 +90,17 @@ type LoggerConfig struct { Compress bool `mapstructure:"compress"` // 是否压缩 UseColor bool `mapstructure:"use_color"` // 是否使用彩色输出 UseDaily bool `mapstructure:"use_daily"` // 是否按日分包 + // 按级别分文件配置 + EnableLevelSeparation bool `mapstructure:"enable_level_separation"` // 是否启用按级别分文件 + LevelConfigs map[string]LevelFileConfig `mapstructure:"level_configs"` // 各级别配置 +} + +// LevelFileConfig 单个级别文件配置 +type LevelFileConfig struct { + MaxSize int `mapstructure:"max_size"` // 单个文件最大大小(MB) + MaxBackups int `mapstructure:"max_backups"` // 最大备份文件数 + MaxAge int `mapstructure:"max_age"` // 最大保留天数 + Compress bool `mapstructure:"compress"` // 是否压缩 } // JWTConfig JWT配置 diff --git a/internal/container/container.go b/internal/container/container.go index 2ca3bba..732eebe 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -6,6 +6,7 @@ import ( "go.uber.org/fx" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "gorm.io/gorm" "tyapi-server/internal/application/certification" @@ -75,8 +76,28 @@ func NewContainer() *Container { // 基础设施模块 fx.Provide( - // 日志器 - 提供自定义Logger和*zap.Logger - func(cfg *config.Config) (logger.Logger, error) { + // 日志器 - 提供自定义Logger和*zap.Logger + func(cfg *config.Config) (logger.Logger, error) { + if cfg.Logger.EnableLevelSeparation { + // 使用按级别分文件的日志器 + levelConfig := logger.LevelLoggerConfig{ + BaseConfig: logger.Config{ + Level: cfg.Logger.Level, + Format: cfg.Logger.Format, + Output: cfg.Logger.Output, + LogDir: cfg.Logger.LogDir, + MaxSize: cfg.Logger.MaxSize, + MaxBackups: cfg.Logger.MaxBackups, + MaxAge: cfg.Logger.MaxAge, + Compress: cfg.Logger.Compress, + UseDaily: cfg.Logger.UseDaily, + }, + EnableLevelSeparation: true, + LevelConfigs: convertLevelConfigs(cfg.Logger.LevelConfigs), + } + return logger.NewLevelLogger(levelConfig) + } else { + // 使用普通日志器 logCfg := logger.Config{ Level: cfg.Logger.Level, Format: cfg.Logger.Format, @@ -89,16 +110,21 @@ func NewContainer() *Container { UseDaily: cfg.Logger.UseDaily, } return logger.NewLogger(logCfg) - }, - // 提供普通的*zap.Logger(用于大多数场景) - func(log logger.Logger) *zap.Logger { - if zapLogger, ok := log.(*logger.ZapLogger); ok { - return zapLogger.GetZapLogger() - } - // 如果类型转换失败,创建一个默认的zap logger - defaultLogger, _ := zap.NewProduction() - return defaultLogger - }, + } + }, + // 提供普通的*zap.Logger(用于大多数场景) + func(log logger.Logger) *zap.Logger { + if zapLogger, ok := log.(*logger.ZapLogger); ok { + return zapLogger.GetZapLogger() + } + // 如果类型转换失败,创建一个默认的zap logger + defaultLogger, _ := zap.NewProduction() + return defaultLogger + }, + // 提供业务日志记录器 + func(logger *zap.Logger) *middleware.BusinessLogger { + return middleware.NewBusinessLogger(logger) + }, // 数据库连接 func(cfg *config.Config, cacheService interfaces.CacheService, logger *zap.Logger) (*gorm.DB, error) { dbCfg := database.Config{ @@ -532,6 +558,8 @@ func RegisterLifecycleHooks( // RegisterMiddlewares 注册中间件 func RegisterMiddlewares( router *sharedhttp.GinRouter, + panicRecovery *middleware.PanicRecoveryMiddleware, + comprehensiveLogger *middleware.ComprehensiveLoggerMiddleware, requestID *middleware.RequestIDMiddleware, security *middleware.SecurityHeadersMiddleware, responseTime *middleware.ResponseTimeMiddleware, @@ -542,6 +570,9 @@ func RegisterMiddlewares( errorTrackingMiddleware *middleware.ErrorTrackingMiddleware, requestBodyLogger *middleware.RequestBodyLoggerMiddleware, ) { + // 注册所有中间件(按优先级顺序) + router.RegisterMiddleware(panicRecovery) + router.RegisterMiddleware(comprehensiveLogger) router.RegisterMiddleware(requestID) router.RegisterMiddleware(security) router.RegisterMiddleware(responseTime) @@ -593,6 +624,26 @@ func RegisterRoutes( // ================ 中间件包装函数 ================ +// NewPanicRecoveryMiddlewareWrapper 创建Panic恢复中间件包装器 +func NewPanicRecoveryMiddlewareWrapper(logger *zap.Logger) *middleware.PanicRecoveryMiddleware { + return middleware.NewPanicRecoveryMiddleware(logger) +} + +// NewComprehensiveLoggerMiddlewareWrapper 创建全面日志中间件包装器 +func NewComprehensiveLoggerMiddlewareWrapper(logger *zap.Logger, cfg *config.Config) *middleware.ComprehensiveLoggerMiddleware { + config := &middleware.ComprehensiveLoggerConfig{ + EnableRequestLogging: true, + EnableResponseLogging: true, + EnableRequestBodyLogging: cfg.App.IsDevelopment(), // 开发环境记录请求体 + EnableErrorLogging: true, + EnableBusinessLogging: true, + EnablePerformanceLogging: true, + MaxBodySize: 1024 * 10, // 10KB + ExcludePaths: []string{"/health", "/metrics", "/favicon.ico", "/swagger"}, + } + return middleware.NewComprehensiveLoggerMiddleware(logger, config) +} + // NewRequestLoggerMiddlewareWrapper 创建请求日志中间件包装器 func NewRequestLoggerMiddlewareWrapper(logger *zap.Logger, cfg *config.Config, tracer *tracing.Tracer) *middleware.RequestLoggerMiddleware { return middleware.NewRequestLoggerMiddleware(logger, cfg.App.IsDevelopment(), tracer) @@ -603,6 +654,35 @@ func NewRequestBodyLoggerMiddlewareWrapper(logger *zap.Logger, cfg *config.Confi return middleware.NewRequestBodyLoggerMiddleware(logger, cfg.App.IsDevelopment(), tracer) } +// ================ 辅助函数 ================ + +// convertLevelConfigs 转换级别配置 +func convertLevelConfigs(configs map[string]config.LevelFileConfig) map[zapcore.Level]logger.LevelFileConfig { + result := make(map[zapcore.Level]logger.LevelFileConfig) + + levelMap := map[string]zapcore.Level{ + "debug": zapcore.DebugLevel, + "info": zapcore.InfoLevel, + "warn": zapcore.WarnLevel, + "error": zapcore.ErrorLevel, + "fatal": zapcore.FatalLevel, + "panic": zapcore.PanicLevel, + } + + for levelStr, config := range configs { + if level, exists := levelMap[levelStr]; exists { + result[level] = logger.LevelFileConfig{ + MaxSize: config.MaxSize, + MaxBackups: config.MaxBackups, + MaxAge: config.MaxAge, + Compress: config.Compress, + } + } + } + + return result +} + // ================ Redis相关工厂函数 ================ // NewRedisClient 创建Redis客户端 diff --git a/internal/shared/logger/level_logger.go b/internal/shared/logger/level_logger.go new file mode 100644 index 0000000..a5d8267 --- /dev/null +++ b/internal/shared/logger/level_logger.go @@ -0,0 +1,314 @@ +package logger + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +// LevelLoggerConfig 按级别分文件的日志配置 +type LevelLoggerConfig struct { + BaseConfig Config + // 是否启用按级别分文件 + EnableLevelSeparation bool + // 各级别日志文件配置 + LevelConfigs map[zapcore.Level]LevelFileConfig +} + +// LevelFileConfig 单个级别文件配置 +type LevelFileConfig struct { + MaxSize int // 单个文件最大大小(MB) + MaxBackups int // 最大备份文件数 + MaxAge int // 最大保留天数 + Compress bool // 是否压缩 +} + +// LevelLogger 按级别分文件的日志器 +type LevelLogger struct { + loggers map[zapcore.Level]*zap.Logger + config LevelLoggerConfig +} + +// NewLevelLogger 创建按级别分文件的日志器 +func NewLevelLogger(config LevelLoggerConfig) (*LevelLogger, error) { + if !config.EnableLevelSeparation { + // 如果不启用按级别分文件,使用普通日志器 + normalLogger, err := NewLogger(config.BaseConfig) + if err != nil { + return nil, err + } + + // 转换为LevelLogger格式 + zapLogger := normalLogger.(*ZapLogger).GetZapLogger() + return &LevelLogger{ + loggers: map[zapcore.Level]*zap.Logger{ + zapcore.DebugLevel: zapLogger, + zapcore.InfoLevel: zapLogger, + zapcore.WarnLevel: zapLogger, + zapcore.ErrorLevel: zapLogger, + zapcore.FatalLevel: zapLogger, + zapcore.PanicLevel: zapLogger, + }, + config: config, + }, nil + } + + // 设置默认级别配置 + if config.LevelConfigs == nil { + config.LevelConfigs = getDefaultLevelConfigs() + } + + // 确保日志目录存在 + if err := os.MkdirAll(config.BaseConfig.LogDir, 0755); err != nil { + return nil, fmt.Errorf("创建日志目录失败: %w", err) + } + + // 为每个级别创建独立的日志器 + loggers := make(map[zapcore.Level]*zap.Logger) + + for level := range config.LevelConfigs { + logger, err := createLevelLogger(level, config) + if err != nil { + return nil, fmt.Errorf("创建级别日志器失败 [%s]: %w", level.String(), err) + } + loggers[level] = logger + } + + return &LevelLogger{ + loggers: loggers, + config: config, + }, nil +} + +// getDefaultLevelConfigs 获取默认级别配置 +func getDefaultLevelConfigs() map[zapcore.Level]LevelFileConfig { + return map[zapcore.Level]LevelFileConfig{ + zapcore.DebugLevel: { + MaxSize: 50, // 50MB + MaxBackups: 3, + MaxAge: 7, // 7天 + Compress: true, + }, + zapcore.InfoLevel: { + MaxSize: 100, // 100MB + MaxBackups: 5, + MaxAge: 30, // 30天 + Compress: true, + }, + zapcore.WarnLevel: { + MaxSize: 100, // 100MB + MaxBackups: 5, + MaxAge: 30, // 30天 + Compress: true, + }, + zapcore.ErrorLevel: { + MaxSize: 200, // 200MB + MaxBackups: 10, + MaxAge: 90, // 90天 + Compress: true, + }, + zapcore.FatalLevel: { + MaxSize: 100, // 100MB + MaxBackups: 10, + MaxAge: 365, // 1年 + Compress: true, + }, + zapcore.PanicLevel: { + MaxSize: 100, // 100MB + MaxBackups: 10, + MaxAge: 365, // 1年 + Compress: true, + }, + } +} + +// createLevelLogger 为单个级别创建日志器 +func createLevelLogger(level zapcore.Level, config LevelLoggerConfig) (*zap.Logger, error) { + levelConfig := config.LevelConfigs[level] + + // 创建编码器 + encoderConfig := getEncoderConfig() + var encoder zapcore.Encoder + if config.BaseConfig.Format == "json" { + encoder = zapcore.NewJSONEncoder(encoderConfig) + } else { + encoder = zapcore.NewConsoleEncoder(encoderConfig) + } + + // 创建文件输出 + writeSyncer, err := createLevelFileWriteSyncer(level, config.BaseConfig, levelConfig) + if err != nil { + return nil, err + } + + // 创建核心 + core := zapcore.NewCore(encoder, writeSyncer, level) + + // 创建日志器 + logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) + + return logger, nil +} + +// createLevelFileWriteSyncer 创建级别文件输出同步器 +func createLevelFileWriteSyncer(level zapcore.Level, baseConfig Config, levelConfig LevelFileConfig) (zapcore.WriteSyncer, error) { + // 构建日志文件路径 + var logFilePath string + if baseConfig.UseDaily { + // 按日分包:logs/2024-01-01/error.log + today := time.Now().Format("2006-01-02") + dailyDir := filepath.Join(baseConfig.LogDir, today) + if err := os.MkdirAll(dailyDir, 0755); err != nil { + return nil, fmt.Errorf("创建日期目录失败: %w", err) + } + logFilePath = filepath.Join(dailyDir, fmt.Sprintf("%s.log", level.String())) + } else { + // 传统方式:logs/error.log + logFilePath = filepath.Join(baseConfig.LogDir, fmt.Sprintf("%s.log", level.String())) + } + + // 创建lumberjack日志轮转器 + lumberJackLogger := &lumberjack.Logger{ + Filename: logFilePath, + MaxSize: levelConfig.MaxSize, + MaxBackups: levelConfig.MaxBackups, + MaxAge: levelConfig.MaxAge, + Compress: levelConfig.Compress, + } + + return zapcore.AddSync(lumberJackLogger), nil +} + +// Debug 调试日志 +func (l *LevelLogger) Debug(msg string, fields ...zapcore.Field) { + if logger, exists := l.loggers[zapcore.DebugLevel]; exists { + logger.Debug(msg, fields...) + } +} + +// Info 信息日志 +func (l *LevelLogger) Info(msg string, fields ...zapcore.Field) { + if logger, exists := l.loggers[zapcore.InfoLevel]; exists { + logger.Info(msg, fields...) + } +} + +// Warn 警告日志 +func (l *LevelLogger) Warn(msg string, fields ...zapcore.Field) { + if logger, exists := l.loggers[zapcore.WarnLevel]; exists { + logger.Warn(msg, fields...) + } +} + +// Error 错误日志 +func (l *LevelLogger) Error(msg string, fields ...zapcore.Field) { + if logger, exists := l.loggers[zapcore.ErrorLevel]; exists { + logger.Error(msg, fields...) + } +} + +// Fatal 致命错误日志 +func (l *LevelLogger) Fatal(msg string, fields ...zapcore.Field) { + if logger, exists := l.loggers[zapcore.FatalLevel]; exists { + logger.Fatal(msg, fields...) + } +} + +// Panic 恐慌日志 +func (l *LevelLogger) Panic(msg string, fields ...zapcore.Field) { + if logger, exists := l.loggers[zapcore.PanicLevel]; exists { + logger.Panic(msg, fields...) + } +} + +// With 添加字段 +func (l *LevelLogger) With(fields ...zapcore.Field) Logger { + // 为每个级别创建带字段的日志器 + newLoggers := make(map[zapcore.Level]*zap.Logger) + for level, logger := range l.loggers { + newLoggers[level] = logger.With(fields...) + } + + return &LevelLogger{ + loggers: newLoggers, + config: l.config, + } +} + +// WithContext 从上下文添加字段 +func (l *LevelLogger) WithContext(ctx context.Context) Logger { + // 从上下文中提取常用字段 + fields := []zapcore.Field{} + + if traceID := getTraceIDFromContextLevel(ctx); traceID != "" { + fields = append(fields, zap.String("trace_id", traceID)) + } + + if userID := getUserIDFromContextLevel(ctx); userID != "" { + fields = append(fields, zap.String("user_id", userID)) + } + + if requestID := getRequestIDFromContextLevel(ctx); requestID != "" { + fields = append(fields, zap.String("request_id", requestID)) + } + + return l.With(fields...) +} + +// Sync 同步日志 +func (l *LevelLogger) Sync() error { + var lastErr error + for _, logger := range l.loggers { + if err := logger.Sync(); err != nil { + lastErr = err + } + } + return lastErr +} + +// GetLevelLogger 获取指定级别的日志器 +func (l *LevelLogger) GetLevelLogger(level zapcore.Level) *zap.Logger { + if logger, exists := l.loggers[level]; exists { + return logger + } + return nil +} + +// GetLoggers 获取所有级别的日志器 +func (l *LevelLogger) GetLoggers() map[zapcore.Level]*zap.Logger { + return l.loggers +} + +// 辅助函数(从logger.go复制) +func getTraceIDFromContextLevel(ctx context.Context) string { + if traceID := ctx.Value("trace_id"); traceID != nil { + if id, ok := traceID.(string); ok { + return id + } + } + return "" +} + +func getUserIDFromContextLevel(ctx context.Context) string { + if userID := ctx.Value("user_id"); userID != nil { + if id, ok := userID.(string); ok { + return id + } + } + return "" +} + +func getRequestIDFromContextLevel(ctx context.Context) string { + if requestID := ctx.Value("request_id"); requestID != nil { + if id, ok := requestID.(string); ok { + return id + } + } + return "" +} \ No newline at end of file diff --git a/internal/shared/middleware/comprehensive_logger.go b/internal/shared/middleware/comprehensive_logger.go new file mode 100644 index 0000000..23f04ee --- /dev/null +++ b/internal/shared/middleware/comprehensive_logger.go @@ -0,0 +1,442 @@ +package middleware + +import ( + "bytes" + "context" + "io" + "strings" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// ComprehensiveLoggerMiddleware 全面日志中间件 +type ComprehensiveLoggerMiddleware struct { + logger *zap.Logger + config *ComprehensiveLoggerConfig +} + +// ComprehensiveLoggerConfig 全面日志配置 +type ComprehensiveLoggerConfig struct { + EnableRequestLogging bool // 是否记录请求日志 + EnableResponseLogging bool // 是否记录响应日志 + EnableRequestBodyLogging bool // 是否记录请求体 + EnableErrorLogging bool // 是否记录错误日志 + EnableBusinessLogging bool // 是否记录业务日志 + EnablePerformanceLogging bool // 是否记录性能日志 + MaxBodySize int64 // 最大记录体大小 + ExcludePaths []string // 排除的路径 + IncludePaths []string // 包含的路径 +} + +// NewComprehensiveLoggerMiddleware 创建全面日志中间件 +func NewComprehensiveLoggerMiddleware(logger *zap.Logger, config *ComprehensiveLoggerConfig) *ComprehensiveLoggerMiddleware { + if config == nil { + config = &ComprehensiveLoggerConfig{ + EnableRequestLogging: true, + EnableResponseLogging: true, + EnableRequestBodyLogging: false, // 生产环境默认关闭 + EnableErrorLogging: true, + EnableBusinessLogging: true, + EnablePerformanceLogging: true, + MaxBodySize: 1024 * 10, // 10KB + ExcludePaths: []string{"/health", "/metrics", "/favicon.ico"}, + } + } + + return &ComprehensiveLoggerMiddleware{ + logger: logger, + config: config, + } +} + +// GetName 返回中间件名称 +func (m *ComprehensiveLoggerMiddleware) GetName() string { + return "comprehensive_logger" +} + +// GetPriority 返回中间件优先级 +func (m *ComprehensiveLoggerMiddleware) GetPriority() int { + return 90 // 高优先级,在panic恢复之后 +} + +// Handle 返回中间件处理函数 +func (m *ComprehensiveLoggerMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + // 检查是否应该记录此路径 + if !m.shouldLogPath(c.Request.URL.Path) { + c.Next() + return + } + + startTime := time.Now() + requestID := c.GetString("request_id") + traceID := c.GetString("trace_id") + userID := c.GetString("user_id") + + // 记录请求开始 + if m.config.EnableRequestLogging { + m.logRequest(c, startTime, requestID, traceID, userID) + } + + // 捕获请求体(如果需要) + var requestBody []byte + if m.config.EnableRequestBodyLogging && m.shouldLogRequestBody(c) { + requestBody = m.captureRequestBody(c) + } + + // 创建响应写入器包装器 + responseWriter := &responseWriterWrapper{ + ResponseWriter: c.Writer, + logger: m.logger, + config: m.config, + requestID: requestID, + traceID: traceID, + userID: userID, + startTime: startTime, + path: c.Request.URL.Path, + method: c.Request.Method, + } + c.Writer = responseWriter + + // 处理请求 + c.Next() + + // 记录响应 + if m.config.EnableResponseLogging { + m.logResponse(c, responseWriter, startTime, requestID, traceID, userID, requestBody) + } + + // 记录错误 + if m.config.EnableErrorLogging && len(c.Errors) > 0 { + m.logErrors(c, requestID, traceID, userID) + } + + // 记录性能指标 + if m.config.EnablePerformanceLogging { + m.logPerformance(c, startTime, requestID, traceID, userID) + } + } +} + +// IsGlobal 是否为全局中间件 +func (m *ComprehensiveLoggerMiddleware) IsGlobal() bool { + return true +} + +// shouldLogPath 检查是否应该记录此路径 +func (m *ComprehensiveLoggerMiddleware) shouldLogPath(path string) bool { + // 检查排除路径 + for _, excludePath := range m.config.ExcludePaths { + if strings.HasPrefix(path, excludePath) { + return false + } + } + + // 检查包含路径(如果指定了) + if len(m.config.IncludePaths) > 0 { + for _, includePath := range m.config.IncludePaths { + if strings.HasPrefix(path, includePath) { + return true + } + } + return false + } + + return true +} + +// shouldLogRequestBody 检查是否应该记录请求体 +func (m *ComprehensiveLoggerMiddleware) shouldLogRequestBody(c *gin.Context) bool { + contentType := c.GetHeader("Content-Type") + return strings.Contains(contentType, "application/json") || + strings.Contains(contentType, "application/x-www-form-urlencoded") +} + +// captureRequestBody 捕获请求体 +func (m *ComprehensiveLoggerMiddleware) captureRequestBody(c *gin.Context) []byte { + if c.Request.Body == nil { + return nil + } + + body, err := io.ReadAll(c.Request.Body) + if err != nil { + return nil + } + + // 重新设置body + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + + // 限制大小 + if int64(len(body)) > m.config.MaxBodySize { + return body[:m.config.MaxBodySize] + } + + return body +} + +// logRequest 记录请求日志 +func (m *ComprehensiveLoggerMiddleware) logRequest(c *gin.Context, startTime time.Time, requestID, traceID, userID string) { + logFields := []zap.Field{ + zap.String("log_type", "request"), + zap.String("request_id", requestID), + zap.String("trace_id", traceID), + zap.String("user_id", userID), + zap.String("method", c.Request.Method), + zap.String("path", c.Request.URL.Path), + zap.String("query", c.Request.URL.RawQuery), + zap.String("client_ip", c.ClientIP()), + zap.String("user_agent", c.Request.UserAgent()), + zap.String("referer", c.Request.Referer()), + zap.Int64("content_length", c.Request.ContentLength), + zap.String("content_type", c.GetHeader("Content-Type")), + zap.Time("timestamp", startTime), + } + + m.logger.Info("收到HTTP请求", logFields...) +} + +// logResponse 记录响应日志 +func (m *ComprehensiveLoggerMiddleware) logResponse(c *gin.Context, responseWriter *responseWriterWrapper, startTime time.Time, requestID, traceID, userID string, requestBody []byte) { + duration := time.Since(startTime) + statusCode := responseWriter.Status() + + logFields := []zap.Field{ + zap.String("log_type", "response"), + zap.String("request_id", requestID), + zap.String("trace_id", traceID), + zap.String("user_id", userID), + zap.String("method", c.Request.Method), + zap.String("path", c.Request.URL.Path), + zap.Int("status_code", statusCode), + zap.Duration("duration", duration), + zap.Int("response_size", responseWriter.Size()), + zap.Time("timestamp", time.Now()), + } + + // 添加请求体(如果记录了) + if len(requestBody) > 0 { + logFields = append(logFields, zap.String("request_body", string(requestBody))) + } + + // 根据状态码选择日志级别 + if statusCode >= 500 { + m.logger.Error("HTTP响应错误", logFields...) + } else if statusCode >= 400 { + m.logger.Warn("HTTP响应警告", logFields...) + } else { + m.logger.Info("HTTP响应成功", logFields...) + } +} + +// logErrors 记录错误日志 +func (m *ComprehensiveLoggerMiddleware) logErrors(c *gin.Context, requestID, traceID, userID string) { + for _, ginErr := range c.Errors { + logFields := []zap.Field{ + zap.String("log_type", "error"), + zap.String("request_id", requestID), + zap.String("trace_id", traceID), + zap.String("user_id", userID), + zap.String("method", c.Request.Method), + zap.String("path", c.Request.URL.Path), + zap.String("error_type", string(ginErr.Type)), + zap.Error(ginErr.Err), + zap.Time("timestamp", time.Now()), + } + + m.logger.Error("请求处理错误", logFields...) + } +} + +// logPerformance 记录性能日志 +func (m *ComprehensiveLoggerMiddleware) logPerformance(c *gin.Context, startTime time.Time, requestID, traceID, userID string) { + duration := time.Since(startTime) + + // 记录慢请求 + if duration > 1*time.Second { + logFields := []zap.Field{ + zap.String("log_type", "performance"), + zap.String("performance_type", "slow_request"), + zap.String("request_id", requestID), + zap.String("trace_id", traceID), + zap.String("user_id", userID), + zap.String("method", c.Request.Method), + zap.String("path", c.Request.URL.Path), + zap.Duration("duration", duration), + zap.Time("timestamp", time.Now()), + } + + m.logger.Warn("检测到慢请求", logFields...) + } + + // 记录性能指标 + logFields := []zap.Field{ + zap.String("log_type", "performance"), + zap.String("performance_type", "request_metrics"), + zap.String("request_id", requestID), + zap.String("trace_id", traceID), + zap.String("user_id", userID), + zap.String("method", c.Request.Method), + zap.String("path", c.Request.URL.Path), + zap.Duration("duration", duration), + zap.Time("timestamp", time.Now()), + } + + m.logger.Debug("请求性能指标", logFields...) +} + +// responseWriterWrapper 响应写入器包装器 +type responseWriterWrapper struct { + gin.ResponseWriter + logger *zap.Logger + config *ComprehensiveLoggerConfig + requestID string + traceID string + userID string + startTime time.Time + path string + method string + status int + size int +} + +// Write 实现Write方法 +func (w *responseWriterWrapper) Write(b []byte) (int, error) { + size, err := w.ResponseWriter.Write(b) + w.size += size + return size, err +} + +// WriteHeader 实现WriteHeader方法 +func (w *responseWriterWrapper) WriteHeader(code int) { + w.status = code + w.ResponseWriter.WriteHeader(code) +} + +// WriteString 实现WriteString方法 +func (w *responseWriterWrapper) WriteString(s string) (int, error) { + size, err := w.ResponseWriter.WriteString(s) + w.size += size + return size, err +} + +// Status 获取状态码 +func (w *responseWriterWrapper) Status() int { + return w.status +} + +// Size 获取响应大小 +func (w *responseWriterWrapper) Size() int { + return w.size +} + +// BusinessLogger 业务日志记录器 +type BusinessLogger struct { + logger *zap.Logger +} + +// NewBusinessLogger 创建业务日志记录器 +func NewBusinessLogger(logger *zap.Logger) *BusinessLogger { + return &BusinessLogger{ + logger: logger, + } +} + +// LogUserAction 记录用户操作 +func (bl *BusinessLogger) LogUserAction(ctx context.Context, action string, details map[string]interface{}) { + requestID := bl.getRequestIDFromContext(ctx) + traceID := bl.getTraceIDFromContext(ctx) + userID := bl.getUserIDFromContext(ctx) + + logFields := []zap.Field{ + zap.String("log_type", "business"), + zap.String("business_type", "user_action"), + zap.String("action", action), + zap.String("request_id", requestID), + zap.String("trace_id", traceID), + zap.String("user_id", userID), + zap.Time("timestamp", time.Now()), + } + + // 添加详细信息 + for key, value := range details { + logFields = append(logFields, zap.Any(key, value)) + } + + bl.logger.Info("用户操作记录", logFields...) +} + +// LogBusinessEvent 记录业务事件 +func (bl *BusinessLogger) LogBusinessEvent(ctx context.Context, event string, details map[string]interface{}) { + requestID := bl.getRequestIDFromContext(ctx) + traceID := bl.getTraceIDFromContext(ctx) + userID := bl.getUserIDFromContext(ctx) + + logFields := []zap.Field{ + zap.String("log_type", "business"), + zap.String("business_type", "business_event"), + zap.String("event", event), + zap.String("request_id", requestID), + zap.String("trace_id", traceID), + zap.String("user_id", userID), + zap.Time("timestamp", time.Now()), + } + + // 添加详细信息 + for key, value := range details { + logFields = append(logFields, zap.Any(key, value)) + } + + bl.logger.Info("业务事件记录", logFields...) +} + +// LogSystemEvent 记录系统事件 +func (bl *BusinessLogger) LogSystemEvent(ctx context.Context, event string, details map[string]interface{}) { + requestID := bl.getRequestIDFromContext(ctx) + traceID := bl.getTraceIDFromContext(ctx) + + logFields := []zap.Field{ + zap.String("log_type", "business"), + zap.String("business_type", "system_event"), + zap.String("event", event), + zap.String("request_id", requestID), + zap.String("trace_id", traceID), + zap.Time("timestamp", time.Now()), + } + + // 添加详细信息 + for key, value := range details { + logFields = append(logFields, zap.Any(key, value)) + } + + bl.logger.Info("系统事件记录", logFields...) +} + +// 辅助方法 +func (bl *BusinessLogger) getRequestIDFromContext(ctx context.Context) string { + if requestID := ctx.Value("request_id"); requestID != nil { + if id, ok := requestID.(string); ok { + return id + } + } + return "" +} + +func (bl *BusinessLogger) getTraceIDFromContext(ctx context.Context) string { + if traceID := ctx.Value("trace_id"); traceID != nil { + if id, ok := traceID.(string); ok { + return id + } + } + return "" +} + +func (bl *BusinessLogger) getUserIDFromContext(ctx context.Context) string { + if userID := ctx.Value("user_id"); userID != nil { + if id, ok := userID.(string); ok { + return id + } + } + return "" +} \ No newline at end of file diff --git a/internal/shared/middleware/panic_recovery.go b/internal/shared/middleware/panic_recovery.go new file mode 100644 index 0000000..3d74b5b --- /dev/null +++ b/internal/shared/middleware/panic_recovery.go @@ -0,0 +1,104 @@ +package middleware + +import ( + "net/http" + "runtime/debug" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// PanicRecoveryMiddleware Panic恢复中间件 +type PanicRecoveryMiddleware struct { + logger *zap.Logger +} + +// NewPanicRecoveryMiddleware 创建Panic恢复中间件 +func NewPanicRecoveryMiddleware(logger *zap.Logger) *PanicRecoveryMiddleware { + return &PanicRecoveryMiddleware{ + logger: logger, + } +} + +// GetName 返回中间件名称 +func (m *PanicRecoveryMiddleware) GetName() string { + return "panic_recovery" +} + +// GetPriority 返回中间件优先级 +func (m *PanicRecoveryMiddleware) GetPriority() int { + return 100 // 最高优先级,第一个执行 +} + +// Handle 返回中间件处理函数 +func (m *PanicRecoveryMiddleware) Handle() gin.HandlerFunc { + return gin.RecoveryWithWriter(&panicLogger{logger: m.logger}) +} + +// IsGlobal 是否为全局中间件 +func (m *PanicRecoveryMiddleware) IsGlobal() bool { + return true +} + +// panicLogger 实现io.Writer接口,用于记录panic信息 +type panicLogger struct { + logger *zap.Logger +} + +// Write 实现io.Writer接口 +func (pl *panicLogger) Write(p []byte) (n int, err error) { + pl.logger.Error("系统发生严重错误", + zap.String("error_type", "panic"), + zap.String("stack_trace", string(p)), + zap.String("timestamp", time.Now().Format("2006-01-02 15:04:05")), + ) + return len(p), nil +} + +// CustomPanicRecovery 自定义panic恢复中间件 +func (m *PanicRecoveryMiddleware) CustomPanicRecovery() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + // 获取请求信息 + requestID := c.GetString("request_id") + traceID := c.GetString("trace_id") + userID := c.GetString("user_id") + clientIP := c.ClientIP() + method := c.Request.Method + path := c.Request.URL.Path + userAgent := c.Request.UserAgent() + + // 记录详细的panic信息 + m.logger.Error("系统发生严重错误", + zap.Any("panic_error", err), + zap.String("error_type", "panic"), + zap.String("request_id", requestID), + zap.String("trace_id", traceID), + zap.String("user_id", userID), + zap.String("client_ip", clientIP), + zap.String("method", method), + zap.String("path", path), + zap.String("user_agent", userAgent), + zap.String("stack_trace", string(debug.Stack())), + zap.String("timestamp", time.Now().Format("2006-01-02 15:04:05")), + ) + + // 返回500错误响应 + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "服务器内部错误", + "error_code": "INTERNAL_SERVER_ERROR", + "request_id": requestID, + "timestamp": time.Now().Unix(), + }) + + // 中止请求处理 + c.Abort() + } + }() + + c.Next() + } +} \ No newline at end of file