diff --git a/.gitignore b/.gitignore index ebefbf8..ecc13c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,66 +1,39 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib +# 日志文件 +logs/ +*.log -# Test binary, built with `go test -c` -*.test +# 编译输出 +bin/ +dist/ -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -vendor/ - -# Go workspace file -go.work - -# IDE +# IDE文件 .vscode/ .idea/ *.swp *.swo -*~ -# OS +# 操作系统文件 .DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db Thumbs.db -# Local environment files +# 环境变量文件 .env .env.local -.env.*.local +.env.production -# Log files -*.log -logs/ - -# Database -*.db -*.sqlite -*.sqlite3 - -# Temporary files +# 临时文件 tmp/ temp/ -# Build directories -build/ -dist/ +# 依赖目录 +vendor/ -# Temporary directories -tmp/ +# 测试覆盖率文件 +coverage.out +coverage.html -# Compiled binary -main -tyapi-server - -# Docker volumes -docker-data/ \ No newline at end of file +# 其他 +*.exe +*.dll +*.so +*.dylib \ No newline at end of file diff --git a/config.yaml b/config.yaml index dcd103b..f9dbf27 100644 --- a/config.yaml +++ b/config.yaml @@ -48,12 +48,13 @@ logger: level: "info" format: "console" output: "stdout" - file_path: "logs/app.log" + log_dir: "logs" max_size: 100 max_backups: 3 max_age: 7 compress: true use_color: true + use_daily: false jwt: secret: "default-jwt-secret-key-change-in-env-config" diff --git a/configs/env.production.yaml b/configs/env.production.yaml index 34059fc..5f2fbac 100644 --- a/configs/env.production.yaml +++ b/configs/env.production.yaml @@ -40,6 +40,13 @@ redis: logger: level: warn format: json + output: "file" + log_dir: "/app/logs" + max_size: 100 + max_backups: 5 + max_age: 30 + compress: true + use_daily: true # =========================================== # 🔐 JWT配置 # =========================================== diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 03b8906..47fb0e2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -122,7 +122,7 @@ services: ports: - "25000:8080" volumes: - - app_logs:/app/logs + - ./logs:/app/logs networks: - tyapi-network depends_on: @@ -163,8 +163,6 @@ volumes: driver: local redis_data: driver: local - app_logs: - driver: local networks: tyapi-network: diff --git a/docs/日志系统说明.md b/docs/日志系统说明.md new file mode 100644 index 0000000..62c5c2c --- /dev/null +++ b/docs/日志系统说明.md @@ -0,0 +1,298 @@ +# 📝 日志系统说明 + +## 概述 + +本项目使用增强的日志系统,支持按日分包和按大小分包,使用 `lumberjack` 进行日志轮转管理。 + +## 功能特性 + +### ✅ 按日分包 +- 日志按日期自动分包存储 +- 目录结构:`logs/2024-01-01/app.log` +- 便于按日期查找和管理日志 + +### ✅ 按大小分包 +- 单个日志文件最大大小限制(默认100MB) +- 超过大小限制时自动创建新文件 +- 文件命名:`app.log`, `app.log.1`, `app.log.2` 等 + +### ✅ 自动轮转 +- 支持压缩旧日志文件 +- 自动删除超过保留期限的日志 +- 限制备份文件数量 + +### ✅ 生产环境持久化 +- 日志目录挂载到宿主机 `./logs` +- 容器重启后日志不丢失 +- 便于日志收集和分析 + +## 配置说明 + +### 基础配置 (`config.yaml`) + +```yaml +logger: + level: "info" # 日志级别 + format: "console" # 日志格式 (console/json) + output: "stdout" # 输出目标 (stdout/stderr/file) + log_dir: "logs" # 日志目录 + max_size: 100 # 单个文件最大大小(MB) + max_backups: 3 # 最大备份文件数 + max_age: 7 # 最大保留天数 + compress: true # 是否压缩 + use_color: true # 是否使用彩色输出 + use_daily: false # 是否按日分包 +``` + +### 生产环境配置 (`configs/env.production.yaml`) + +```yaml +logger: + level: warn # 生产环境只记录警告及以上级别 + format: json # JSON格式便于日志分析 + output: "file" # 输出到文件 + log_dir: "/app/logs" # 容器内日志目录 + max_size: 100 # 100MB + max_backups: 5 # 5个备份文件 + max_age: 30 # 保留30天 + compress: true # 启用压缩 + use_daily: true # 启用按日分包 +``` + +## 日志目录结构 + +### 按日分包模式 +``` +logs/ +├── 2024-01-01/ +│ ├── app.log # 当前日志文件 +│ ├── app.log.1 # 第一个备份文件 +│ ├── app.log.2 # 第二个备份文件 +│ └── app.log.3.gz # 压缩的备份文件 +├── 2024-01-02/ +│ ├── app.log +│ └── app.log.1 +└── 2024-01-03/ + └── app.log +``` + +### 传统模式 +``` +logs/ +├── app.log # 当前日志文件 +├── app.log.1 # 第一个备份文件 +├── app.log.2 # 第二个备份文件 +└── app.log.3.gz # 压缩的备份文件 +``` + +## 日志级别 + +| 级别 | 说明 | 使用场景 | +|------|------|----------| +| `debug` | 调试信息 | 开发环境详细调试 | +| `info` | 一般信息 | 业务流程关键节点 | +| `warn` | 警告信息 | 需要注意但不影响功能 | +| `error` | 错误信息 | 业务逻辑错误 | +| `fatal` | 致命错误 | 系统无法继续运行 | +| `panic` | 恐慌错误 | 程序崩溃 | + +## 日志格式 + +### JSON格式(生产环境) +```json +{ + "timestamp": "2024-01-01T12:00:00Z", + "level": "info", + "message": "用户注册成功", + "user_id": "12345", + "phone": "138****8888", + "request_id": "req-123456", + "caller": "user_handler.go:45" +} +``` + +### 控制台格式(开发环境) +``` +2024-01-01T12:00:00Z INFO 用户注册成功 {"user_id": "12345", "phone": "138****8888"} +``` + +## 使用方法 + +### 1. 查看日志 + +```bash +# 查看实时日志 +docker logs -f tyapi-app-prod + +# 查看最近的日志 +docker logs --tail 100 tyapi-app-prod + +# 查看宿主机日志文件 +tail -f logs/2024-01-01/app.log + +# 查看错误日志 +grep "ERROR" logs/2024-01-01/app.log +``` + +### 2. 日志管理 + +```bash +# 查看日志统计信息 +./scripts/log-manager.sh stats + +# 查看日志目录大小 +./scripts/log-manager.sh size + +# 列出所有日志文件 +./scripts/log-manager.sh list + +# 清理旧日志文件 +./scripts/log-manager.sh clean +``` + +### 3. 日志分析 + +```bash +# 使用jq分析JSON日志 +cat logs/2024-01-01/app.log | jq 'select(.level == "error")' + +# 统计错误类型 +cat logs/2024-01-01/app.log | jq -r '.level' | sort | uniq -c + +# 查看特定用户的请求 +cat logs/2024-01-01/app.log | jq 'select(.user_id == "12345")' +``` + +## 代码示例 + +### 基本日志记录 +```go +import "tyapi-server/internal/shared/logger" + +// 获取日志器 +log := logger.GetLogger() + +// 记录不同级别的日志 +log.Info("用户登录成功", logger.String("user_id", "12345")) +log.Warn("用户多次登录失败", logger.String("phone", "138****8888")) +log.Error("数据库连接失败", logger.Error(err)) +``` + +### 带上下文的日志 +```go +// 从Gin上下文获取日志器 +ctx := c.Request.Context() +log := logger.GetLogger().WithContext(ctx) + +log.Info("处理用户请求", + logger.String("action", "user_login"), + logger.String("ip", c.ClientIP()), +) +``` + +### 结构化日志字段 +```go +log.Info("业务操作", + logger.String("operation", "create_user"), + logger.String("user_id", user.ID), + logger.Int("age", user.Age), + logger.Float64("score", 95.5), + logger.Bool("is_active", true), + logger.Error(err), +) +``` + +## 监控和告警 + +### 1. 日志监控 +- 使用 ELK Stack (Elasticsearch + Logstash + Kibana) +- 或使用 Grafana + Loki +- 实时监控错误日志和性能指标 + +### 2. 告警配置 +- 错误日志数量告警 +- 日志文件大小告警 +- 磁盘空间告警 + +### 3. 性能监控 +```bash +# 监控日志写入性能 +iostat -x 1 + +# 监控磁盘使用情况 +df -h logs/ + +# 监控日志文件增长 +watch -n 1 'ls -lh logs/$(date +%Y-%m-%d)/app.log' +``` + +## 故障排除 + +### 常见问题 + +1. **日志文件权限问题** + ```bash + # 修复权限 + chmod -R 755 logs/ + chown -R $(whoami) logs/ + ``` + +2. **磁盘空间不足** + ```bash + # 清理旧日志 + ./scripts/log-manager.sh clean + + # 检查磁盘使用情况 + df -h + ``` + +3. **日志轮转失败** + ```bash + # 检查lumberjack配置 + # 确保有足够的磁盘空间 + # 检查文件权限 + ``` + +### 性能优化 + +1. **异步日志写入** + - 使用缓冲写入减少I/O操作 + - 批量写入提高性能 + +2. **日志级别控制** + - 生产环境使用 `warn` 级别 + - 减少不必要的日志输出 + +3. **定期清理** + - 设置合理的保留期限 + - 定期清理旧日志文件 + +## 最佳实践 + +### 1. 日志内容 +- ✅ 记录关键业务操作 +- ✅ 包含足够的上下文信息 +- ✅ 使用结构化的字段 +- ❌ 避免记录敏感信息 +- ❌ 避免过度详细的调试信息 + +### 2. 日志管理 +- ✅ 定期清理旧日志 +- ✅ 监控日志文件大小 +- ✅ 设置合理的轮转策略 +- ✅ 备份重要日志 + +### 3. 性能考虑 +- ✅ 使用异步日志写入 +- ✅ 合理设置日志级别 +- ✅ 避免在循环中记录日志 +- ✅ 使用结构化日志字段 + +## 相关文件 + +- `internal/shared/logger/logger.go` - 日志系统核心实现 +- `internal/config/config.go` - 日志配置结构 +- `config.yaml` - 基础日志配置 +- `configs/env.production.yaml` - 生产环境日志配置 +- `docker-compose.prod.yml` - Docker日志卷挂载配置 +- `scripts/log-manager.sh` - 日志管理脚本 \ No newline at end of file diff --git a/go.mod b/go.mod index 9eabcd5..adadce4 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( go.uber.org/fx v1.24.0 go.uber.org/zap v1.27.0 golang.org/x/time v0.12.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.30.0 ) diff --git a/go.sum b/go.sum index 7c88e14..3a4a34d 100644 --- a/go.sum +++ b/go.sum @@ -369,6 +369,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/config/config.go b/internal/config/config.go index 62966d9..d90449f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -83,12 +83,13 @@ type LoggerConfig struct { Level string `mapstructure:"level"` Format string `mapstructure:"format"` Output string `mapstructure:"output"` - FilePath string `mapstructure:"file_path"` - MaxSize int `mapstructure:"max_size"` - MaxBackups int `mapstructure:"max_backups"` - MaxAge int `mapstructure:"max_age"` - Compress bool `mapstructure:"compress"` - UseColor bool `mapstructure:"use_color"` + LogDir string `mapstructure:"log_dir"` // 日志目录 + MaxSize int `mapstructure:"max_size"` // 单个文件最大大小(MB) + MaxBackups int `mapstructure:"max_backups"` // 最大备份文件数 + MaxAge int `mapstructure:"max_age"` // 最大保留天数 + Compress bool `mapstructure:"compress"` // 是否压缩 + UseColor bool `mapstructure:"use_color"` // 是否使用彩色输出 + UseDaily bool `mapstructure:"use_daily"` // 是否按日分包 } // JWTConfig JWT配置 diff --git a/internal/container/container.go b/internal/container/container.go index 352f1a6..2ca3bba 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -81,11 +81,12 @@ func NewContainer() *Container { Level: cfg.Logger.Level, Format: cfg.Logger.Format, Output: cfg.Logger.Output, - FilePath: cfg.Logger.FilePath, + LogDir: cfg.Logger.LogDir, MaxSize: cfg.Logger.MaxSize, MaxBackups: cfg.Logger.MaxBackups, MaxAge: cfg.Logger.MaxAge, Compress: cfg.Logger.Compress, + UseDaily: cfg.Logger.UseDaily, } return logger.NewLogger(logCfg) }, diff --git a/internal/shared/logger/logger.go b/internal/shared/logger/logger.go index c27f127..1fd0532 100644 --- a/internal/shared/logger/logger.go +++ b/internal/shared/logger/logger.go @@ -4,9 +4,12 @@ import ( "context" "fmt" "os" + "path/filepath" + "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" ) // Logger 日志接口 @@ -33,11 +36,12 @@ type Config struct { Level string Format string Output string - FilePath string - MaxSize int - MaxBackups int - MaxAge int - Compress bool + LogDir string // 日志目录 + MaxSize int // 单个文件最大大小(MB) + MaxBackups int // 最大备份文件数 + MaxAge int // 最大保留天数 + Compress bool // 是否压缩 + UseDaily bool // 是否按日分包 } // NewLogger 创建新的日志实例 @@ -69,19 +73,10 @@ func NewLogger(config Config) (Logger, error) { case "stderr": writeSyncer = zapcore.AddSync(os.Stderr) case "file": - if config.FilePath == "" { - config.FilePath = "logs/app.log" - } - // 确保目录存在 - if err := os.MkdirAll("logs", 0755); err != nil { - return nil, fmt.Errorf("创建日志目录失败: %w", err) - } - - file, err := os.OpenFile(config.FilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + writeSyncer, err = createFileWriteSyncer(config) if err != nil { - return nil, fmt.Errorf("打开日志文件失败: %w", err) + return nil, fmt.Errorf("创建文件输出失败: %w", err) } - writeSyncer = zapcore.AddSync(file) default: writeSyncer = zapcore.AddSync(os.Stdout) } @@ -97,6 +92,56 @@ func NewLogger(config Config) (Logger, error) { }, nil } +// createFileWriteSyncer 创建文件输出同步器 +func createFileWriteSyncer(config Config) (zapcore.WriteSyncer, error) { + // 设置默认日志目录 + if config.LogDir == "" { + config.LogDir = "logs" + } + + // 确保日志目录存在 + if err := os.MkdirAll(config.LogDir, 0755); err != nil { + return nil, fmt.Errorf("创建日志目录失败: %w", err) + } + + // 设置默认值 + if config.MaxSize == 0 { + config.MaxSize = 100 // 默认100MB + } + if config.MaxBackups == 0 { + config.MaxBackups = 3 // 默认3个备份 + } + if config.MaxAge == 0 { + config.MaxAge = 7 // 默认7天 + } + + // 构建日志文件路径 + var logFilePath string + if config.UseDaily { + // 按日分包:logs/2024-01-01/app.log + today := time.Now().Format("2006-01-02") + dailyDir := filepath.Join(config.LogDir, today) + if err := os.MkdirAll(dailyDir, 0755); err != nil { + return nil, fmt.Errorf("创建日期目录失败: %w", err) + } + logFilePath = filepath.Join(dailyDir, "app.log") + } else { + // 传统方式:logs/app.log + logFilePath = filepath.Join(config.LogDir, "app.log") + } + + // 创建lumberjack日志轮转器 + lumberJackLogger := &lumberjack.Logger{ + Filename: logFilePath, + MaxSize: config.MaxSize, // 单个文件最大大小(MB) + MaxBackups: config.MaxBackups, // 最大备份文件数 + MaxAge: config.MaxAge, // 最大保留天数 + Compress: config.Compress, // 是否压缩 + } + + return zapcore.AddSync(lumberJackLogger), nil +} + // getEncoderConfig 获取编码器配置 func getEncoderConfig() zapcore.EncoderConfig { return zapcore.EncoderConfig{ diff --git a/scripts/log-manager.sh b/scripts/log-manager.sh new file mode 100644 index 0000000..8318ba0 --- /dev/null +++ b/scripts/log-manager.sh @@ -0,0 +1,154 @@ +#!/bin/bash + +# 日志管理脚本 +# 用于清理旧日志文件和查看日志统计信息 + +LOG_DIR="./logs" +RETENTION_DAYS=30 + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 打印带颜色的消息 +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 显示帮助信息 +show_help() { + echo "日志管理脚本" + echo "" + echo "用法: $0 [命令]" + echo "" + echo "命令:" + echo " clean - 清理超过 $RETENTION_DAYS 天的旧日志文件" + echo " stats - 显示日志统计信息" + echo " size - 显示日志目录大小" + echo " list - 列出所有日志文件" + echo " help - 显示此帮助信息" + echo "" + echo "示例:" + echo " $0 clean # 清理旧日志" + echo " $0 stats # 查看统计信息" +} + +# 清理旧日志文件 +clean_old_logs() { + print_info "开始清理超过 $RETENTION_DAYS 天的旧日志文件..." + + if [ ! -d "$LOG_DIR" ]; then + print_error "日志目录 $LOG_DIR 不存在" + return 1 + fi + + # 查找并删除超过指定天数的日志文件 + find "$LOG_DIR" -name "*.log*" -type f -mtime +$RETENTION_DAYS -exec rm -f {} \; + + # 删除空的日期目录 + find "$LOG_DIR" -type d -empty -delete + + print_success "旧日志文件清理完成" +} + +# 显示日志统计信息 +show_stats() { + print_info "日志统计信息:" + echo "" + + if [ ! -d "$LOG_DIR" ]; then + print_error "日志目录 $LOG_DIR 不存在" + return 1 + fi + + # 总文件数 + total_files=$(find "$LOG_DIR" -name "*.log*" -type f | wc -l) + echo "总日志文件数: $total_files" + + # 总大小 + total_size=$(du -sh "$LOG_DIR" 2>/dev/null | cut -f1) + echo "日志目录总大小: $total_size" + + # 按日期统计 + echo "" + echo "按日期统计:" + for date_dir in "$LOG_DIR"/*/; do + if [ -d "$date_dir" ]; then + date_name=$(basename "$date_dir") + file_count=$(find "$date_dir" -name "*.log*" -type f | wc -l) + dir_size=$(du -sh "$date_dir" 2>/dev/null | cut -f1) + echo " $date_name: $file_count 个文件, $dir_size" + fi + done + + # 最近修改的文件 + echo "" + echo "最近修改的日志文件:" + find "$LOG_DIR" -name "*.log*" -type f -exec ls -lh {} \; | head -5 +} + +# 显示日志目录大小 +show_size() { + if [ ! -d "$LOG_DIR" ]; then + print_error "日志目录 $LOG_DIR 不存在" + return 1 + fi + + total_size=$(du -sh "$LOG_DIR" 2>/dev/null | cut -f1) + print_info "日志目录大小: $total_size" +} + +# 列出所有日志文件 +list_logs() { + if [ ! -d "$LOG_DIR" ]; then + print_error "日志目录 $LOG_DIR 不存在" + return 1 + fi + + print_info "所有日志文件:" + find "$LOG_DIR" -name "*.log*" -type f -exec ls -lh {} \; +} + +# 主函数 +main() { + case "$1" in + "clean") + clean_old_logs + ;; + "stats") + show_stats + ;; + "size") + show_size + ;; + "list") + list_logs + ;; + "help"|"-h"|"--help"|"") + show_help + ;; + *) + print_error "未知命令: $1" + show_help + exit 1 + ;; + esac +} + +# 执行主函数 +main "$@" \ No newline at end of file