diff --git a/LOGGING.md b/LOGGING.md new file mode 100644 index 000000000..9a8f17b85 --- /dev/null +++ b/LOGGING.md @@ -0,0 +1,260 @@ +# 日志系统说明 + +本项目使用 Go 标准库的 `log/slog` 实现结构化日志记录。 + +## 📋 功能特性 + +### 1. 标准的文件存储结构 + +- **当前日志文件**: `oneapi.log` - 实时写入的日志文件 +- **归档日志文件**: `oneapi.2024-01-02-153045.log` - 自动轮转后的历史日志 + +### 2. 自动日志轮转 + +日志文件会在以下情况自动轮转: + +- **按大小轮转**: 当日志文件超过指定大小时(默认 100MB) +- **启动时日期检查**: 程序启动时如果检测到日志文件是旧日期创建的,会自动轮转 +- **自动清理**: 只保留最近 N 个日志文件(默认 7 个) + +### 3. 结构化日志 + +所有日志都包含以下结构化字段: + +``` +time=2024-01-02T15:30:45 level=INFO msg="user logged in" request_id=abc123 user_id=1001 +``` + +### 4. 多种输出格式 + +- **Text 格式** (默认): 人类可读的文本格式 +- **JSON 格式**: 便于日志分析工具解析 + +### 5. 灵活的日志级别 + +支持四个日志级别: +- `DEBUG`: 调试信息 +- `INFO`: 一般信息 +- `WARN`: 警告信息 +- `ERROR`: 错误信息 + +## ⚙️ 配置方式 + +### 环境变量配置 + +```bash +# 日志目录(必需,否则只输出到控制台) +--log-dir=./logs + +# 日志级别(可选,默认: INFO,DEBUG 模式除外) +export LOG_LEVEL=DEBUG # 可选值: DEBUG, INFO, WARN, ERROR + +# 日志格式(可选,默认: text) +export LOG_FORMAT=json # 可选值: text, json + +# 单个日志文件最大大小(可选,默认: 100,单位: MB) +export LOG_MAX_SIZE_MB=200 + +# 保留的日志文件数量(可选,默认: 7) +export LOG_MAX_FILES=14 + +# 启用调试模式(会自动将日志级别设为 DEBUG) +export DEBUG=true +``` + +### 命令行参数 + +```bash +# 启动时指定日志目录 +./new-api --log-dir=./logs + +# 如果不指定日志目录,日志只输出到控制台 +./new-api +``` + +## 📝 使用示例 + +### 基础使用 + +```go +import ( + "context" + "github.com/QuantumNous/new-api/logger" +) + +// 记录信息日志 +logger.LogInfo(ctx, "user registered successfully") + +// 记录警告日志 +logger.LogWarn(ctx, "API rate limit approaching") + +// 记录错误日志 +logger.LogError(ctx, "failed to connect to database") + +// 记录调试日志(只在 DEBUG 模式下输出) +logger.LogDebug(ctx, "processing request with params: %v", params) + +// 记录系统日志(无 context) +logger.LogSystemInfo("application started") +logger.LogSystemError("critical system error") +``` + +### 日志输出示例 + +**Text 格式** (易读格式): +``` +[INFO] 2024/01/02 - 15:30:45 | SYSTEM | application started +[INFO] 2024/01/02 - 15:30:46 | abc123 | user registered successfully +[WARN] 2024/01/02 - 15:30:47 | def456 | API rate limit approaching | remaining=10, limit=100 +[ERROR] 2024/01/02 - 15:30:48 | ghi789 | failed to connect to database | error="connection timeout" +``` + +格式说明:`[级别] 时间 | 请求ID/组件 | 消息 | 额外属性(如有)` + +**JSON 格式**: +```json +{"time":"2024-01-02 15:30:45","level":"INFO","msg":"application started","request_id":"SYSTEM"} +{"time":"2024-01-02 15:30:46","level":"INFO","msg":"user registered successfully","request_id":"abc123"} +{"time":"2024-01-02 15:30:47","level":"WARN","msg":"API rate limit approaching","request_id":"def456"} +``` + +## 📂 日志文件结构 + +``` +logs/ +├── oneapi.log # 当前活动日志文件 +├── oneapi.2024-01-01-090000.log # 昨天的日志 +├── oneapi.2024-01-01-150000.log # 昨天下午的日志(如果超过大小限制) +├── oneapi.2023-12-31-090000.log # 更早的日志 +└── ... # 最多保留配置数量的历史文件 +``` + +## 🔄 日志轮转机制 + +### 轮转触发条件 + +1. **文件大小检查**: 每写入 1000 条日志后检查一次文件大小 +2. **启动时日期检查**: 程序启动时检查日志文件的修改日期,如果不是今天则轮转 +3. **自动清理**: 轮转时自动删除超过保留数量的旧日志文件 + +> **注意**: 日志不会在运行时动态检查日期变化。如果需要每天自动轮转日志,建议: +> - 使用定时任务(如 cron)每天重启服务 +> - 或者配置较小的日志文件大小,让它自动按大小轮转 + +### 轮转流程 + +1. 检测到需要轮转时,关闭当前日志文件 +2. 将 `oneapi.log` 重命名为 `oneapi.YYYY-MM-DD-HHmmss.log` +3. 创建新的 `oneapi.log` 文件 +4. 异步清理超过数量限制的旧日志文件 +5. 记录轮转事件到新日志文件 + +## 🎯 最佳实践 + +### 1. 生产环境配置 + +```bash +# 使用 INFO 级别,避免过多调试信息 +export LOG_LEVEL=INFO + +# 使用 JSON 格式,便于日志分析工具处理 +export LOG_FORMAT=json + +# 设置合适的文件大小和保留数量 +export LOG_MAX_SIZE_MB=500 +export LOG_MAX_FILES=30 + +# 指定日志目录 +./new-api --log-dir=/var/log/oneapi +``` + +### 2. 开发环境配置 + +```bash +# 使用 DEBUG 级别查看详细信息 +export DEBUG=true + +# 使用 Text 格式,便于阅读 +export LOG_FORMAT=text + +# 较小的文件大小和保留数量 +export LOG_MAX_SIZE_MB=50 +export LOG_MAX_FILES=7 + +./new-api --log-dir=./logs +``` + +### 3. 容器环境配置 + +```bash +# 只输出到标准输出,由容器运行时管理日志 +./new-api + +# 或者使用 JSON 格式便于日志收集系统处理 +export LOG_FORMAT=json +./new-api +``` + +## 🔍 日志分析 + +### 使用 grep 分析文本日志 + +```bash +# 查找错误日志 +grep '\[ERROR\]' logs/oneapi.log + +# 查找特定请求的所有日志 +grep 'abc123' logs/*.log + +# 查看最近的警告和错误 +tail -f logs/oneapi.log | grep -E '\[(WARN|ERROR)\]' + +# 查找包含特定关键词的日志 +grep 'database' logs/oneapi.log + +# 查看今天的所有错误 +grep "\[ERROR\] $(date +%Y/%m/%d)" logs/oneapi.log +``` + +### 使用 jq 分析 JSON 日志 + +```bash +# 提取所有错误日志 +cat logs/oneapi.log | jq 'select(.level=="ERROR")' + +# 统计各级别日志数量 +cat logs/oneapi.log | jq -r '.level' | sort | uniq -c + +# 查找特定时间范围的日志 +cat logs/oneapi.log | jq 'select(.time >= "2024-01-02 15:00:00" and .time <= "2024-01-02 16:00:00")' +``` + +## 📊 性能优化 + +1. **异步日志轮转**: 轮转操作在后台 goroutine 中执行,不阻塞主程序 +2. **批量写入检查**: 每 1000 次写入才检查一次轮转条件,减少 I/O 开销 +3. **读写锁**: 使用 `sync.RWMutex` 保护日志器,提高并发性能 +4. **零分配**: `slog` 库在大多数情况下实现零内存分配 + +## 🚨 故障排查 + +### 日志文件未创建 + +- 检查日志目录是否存在且有写入权限 +- 确认启动时指定了 `--log-dir` 参数 + +### 日志文件过多 + +- 调整 `LOG_MAX_FILES` 环境变量 +- 手动清理不需要的旧日志文件 + +### 日志级别不正确 + +- 检查 `LOG_LEVEL` 环境变量是否正确设置 +- 确认 `DEBUG` 环境变量的值(会覆盖 LOG_LEVEL) + +## 📖 相关文档 + +- [Go slog 官方文档](https://pkg.go.dev/log/slog) +- [结构化日志最佳实践](https://go.dev/blog/slog) + diff --git a/common/init.go b/common/init.go index 0ae7dcd68..749da42a8 100644 --- a/common/init.go +++ b/common/init.go @@ -3,7 +3,7 @@ package common import ( "flag" "fmt" - "log" + "log/slog" "os" "path/filepath" "strconv" @@ -43,9 +43,10 @@ func InitEnv() { if os.Getenv("SESSION_SECRET") != "" { ss := os.Getenv("SESSION_SECRET") if ss == "random_string" { - log.Println("WARNING: SESSION_SECRET is set to the default value 'random_string', please change it to a random string.") - log.Println("警告:SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。") - log.Fatal("Please set SESSION_SECRET to a random string.") + slog.Warn("SESSION_SECRET is set to the default value 'random_string', please change it to a random string.") + slog.Warn("警告:SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。") + slog.Error("Please set SESSION_SECRET to a random string.") + os.Exit(1) } else { SessionSecret = ss } @@ -62,12 +63,14 @@ func InitEnv() { var err error *LogDir, err = filepath.Abs(*LogDir) if err != nil { - log.Fatal(err) + slog.Error("failed to get absolute path for log directory", "error", err) + os.Exit(1) } if _, err := os.Stat(*LogDir); os.IsNotExist(err) { err = os.Mkdir(*LogDir, 0777) if err != nil { - log.Fatal(err) + slog.Error("failed to create log directory", "error", err) + os.Exit(1) } } } diff --git a/common/sys_log.go b/common/sys_log.go index b29adc3e6..a8db32937 100644 --- a/common/sys_log.go +++ b/common/sys_log.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "log/slog" "os" "time" @@ -9,18 +10,16 @@ import ( ) func SysLog(s string) { - t := time.Now() - _, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) + slog.Info(s, "component", "system") } func SysError(s string) { - t := time.Now() - _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) + slog.Error(s, "component", "system") } func FatalLog(v ...any) { - t := time.Now() - _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v) + msg := fmt.Sprint(v...) + slog.Error(msg, "component", "system", "level", "fatal") os.Exit(1) } diff --git a/logger/logger.go b/logger/logger.go index 61b1d49d8..5fc0031a2 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -5,9 +5,11 @@ import ( "encoding/json" "fmt" "io" - "log" + "log/slog" "os" "path/filepath" + "sort" + "strings" "sync" "time" @@ -19,81 +21,561 @@ import ( ) const ( - loggerINFO = "INFO" - loggerWarn = "WARN" - loggerError = "ERR" - loggerDebug = "DEBUG" + // 日志轮转配置 + defaultMaxLogSize = 100 * 1024 * 1024 // 100MB + defaultMaxLogFiles = 7 // 保留最近7个日志文件 + defaultLogFileName = "newapi.log" + checkRotateInterval = 1000 // 每1000次写入检查一次是否需要轮转 ) -const maxLogCount = 1000000 +var ( + logMutex sync.RWMutex + rotateCheckLock sync.Mutex + defaultLogger *slog.Logger + logFile *os.File + logFilePath string + logDirPath string + writeCount int64 + maxLogSize int64 = defaultMaxLogSize + maxLogFiles int = defaultMaxLogFiles + useJSONFormat bool +) -var logCount int -var setupLogLock sync.Mutex -var setupLogWorking bool +func init() { + // Initialize with a text handler to stdout + handler := createHandler(os.Stdout) + defaultLogger = slog.New(handler) + slog.SetDefault(defaultLogger) +} +// SetupLogger 初始化日志系统 func SetupLogger() { - defer func() { - setupLogWorking = false - }() - if *common.LogDir != "" { - ok := setupLogLock.TryLock() - if !ok { - log.Println("setup log is already working") - return + logMutex.Lock() + defer logMutex.Unlock() + + // 读取环境变量配置 + if maxSize := os.Getenv("LOG_MAX_SIZE_MB"); maxSize != "" { + if size, err := fmt.Sscanf(maxSize, "%d", &maxLogSize); err == nil && size > 0 { + maxLogSize = maxLogSize * 1024 * 1024 // 转换为字节 } - defer func() { - setupLogLock.Unlock() - }() - logPath := filepath.Join(*common.LogDir, fmt.Sprintf("oneapi-%s.log", time.Now().Format("20060102150405"))) - fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - log.Fatal("failed to open log file") + } + if maxFiles := os.Getenv("LOG_MAX_FILES"); maxFiles != "" { + fmt.Sscanf(maxFiles, "%d", &maxLogFiles) + } + if os.Getenv("LOG_FORMAT") == "json" { + useJSONFormat = true + } + + if *common.LogDir == "" { + // 如果没有配置日志目录,只输出到标准输出 + handler := createHandler(os.Stdout) + defaultLogger = slog.New(handler) + slog.SetDefault(defaultLogger) + return + } + + logDirPath = *common.LogDir + logFilePath = filepath.Join(logDirPath, defaultLogFileName) + + // 检查日志文件是否需要按日期轮转(仅在启动时检查) + if err := checkAndRotateOnStartup(); err != nil { + slog.Error("failed to check log file on startup", "error", err) + } + + // 打开或创建日志文件 + if err := openLogFile(); err != nil { + slog.Error("failed to open log file", "error", err) + return + } + + // 创建多路输出(控制台 + 文件) + multiWriter := io.MultiWriter(os.Stdout, logFile) + + // 更新 gin 的默认输出 + gin.DefaultWriter = multiWriter + gin.DefaultErrorWriter = multiWriter + + // 更新 slog handler + handler := createHandler(multiWriter) + defaultLogger = slog.New(handler) + slog.SetDefault(defaultLogger) + + slog.Info("logger initialized", + "log_dir", logDirPath, + "max_size_mb", maxLogSize/(1024*1024), + "max_files", maxLogFiles, + "format", getLogFormat()) +} + +// createHandler 创建日志处理器 +func createHandler(w io.Writer) slog.Handler { + if useJSONFormat { + opts := &slog.HandlerOptions{ + Level: getLogLevel(), } - gin.DefaultWriter = io.MultiWriter(os.Stdout, fd) - gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd) + return slog.NewJSONHandler(w, opts) + } + return NewReadableTextHandler(w, getLogLevel()) +} + +// ReadableTextHandler 自定义的易读文本处理器 +type ReadableTextHandler struct { + w io.Writer + level slog.Level + mu sync.Mutex +} + +// NewReadableTextHandler 创建一个新的易读文本处理器 +func NewReadableTextHandler(w io.Writer, level slog.Level) *ReadableTextHandler { + return &ReadableTextHandler{ + w: w, + level: level, } } -func LogInfo(ctx context.Context, msg string) { - logHelper(ctx, loggerINFO, msg) +// Enabled 检查是否启用该级别 +func (h *ReadableTextHandler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.level } -func LogWarn(ctx context.Context, msg string) { - logHelper(ctx, loggerWarn, msg) +// Handle 处理日志记录 +func (h *ReadableTextHandler) Handle(_ context.Context, r slog.Record) error { + h.mu.Lock() + defer h.mu.Unlock() + + // 格式: [LEVEL] YYYY/MM/DD - HH:mm:ss | request_id | message | key=value ... + buf := make([]byte, 0, 256) + + // 日志级别 + level := r.Level.String() + switch r.Level { + case slog.LevelDebug: + level = "DEBUG" + case slog.LevelInfo: + level = "INFO" + case slog.LevelWarn: + level = "WARN" + case slog.LevelError: + level = "ERROR" + } + buf = append(buf, '[') + buf = append(buf, level...) + buf = append(buf, "] "...) + + // 时间 + buf = append(buf, r.Time.Format("2006/01/02 - 15:04:05")...) + buf = append(buf, " | "...) + + // 提取 request_id 和 component + var requestID, component string + otherAttrs := make([]slog.Attr, 0) + + r.Attrs(func(a slog.Attr) bool { + switch a.Key { + case "request_id": + requestID = a.Value.String() + case "component": + component = a.Value.String() + default: + otherAttrs = append(otherAttrs, a) + } + return true + }) + + // 输出 request_id 或 component + if requestID != "" { + buf = append(buf, requestID...) + buf = append(buf, " | "...) + } else if component != "" { + buf = append(buf, component...) + buf = append(buf, " | "...) + } + + // 消息 + buf = append(buf, r.Message...) + + // 其他属性 + if len(otherAttrs) > 0 { + buf = append(buf, " | "...) + for i, a := range otherAttrs { + if i > 0 { + buf = append(buf, ", "...) + } + buf = append(buf, a.Key...) + buf = append(buf, '=') + buf = appendValue(buf, a.Value) + } + } + + buf = append(buf, '\n') + _, err := h.w.Write(buf) + return err } -func LogError(ctx context.Context, msg string) { - logHelper(ctx, loggerError, msg) +// appendValue 追加值到缓冲区 +func appendValue(buf []byte, v slog.Value) []byte { + switch v.Kind() { + case slog.KindString: + s := v.String() + // 如果字符串包含空格或特殊字符,加引号 + if strings.ContainsAny(s, " \t\n\r,=") { + buf = append(buf, '"') + buf = append(buf, s...) + buf = append(buf, '"') + } else { + buf = append(buf, s...) + } + case slog.KindInt64: + buf = append(buf, fmt.Sprintf("%d", v.Int64())...) + case slog.KindUint64: + buf = append(buf, fmt.Sprintf("%d", v.Uint64())...) + case slog.KindFloat64: + buf = append(buf, fmt.Sprintf("%g", v.Float64())...) + case slog.KindBool: + buf = append(buf, fmt.Sprintf("%t", v.Bool())...) + case slog.KindDuration: + buf = append(buf, v.Duration().String()...) + case slog.KindTime: + buf = append(buf, v.Time().Format("2006-01-02 15:04:05")...) + default: + buf = append(buf, fmt.Sprintf("%v", v.Any())...) + } + return buf } -func LogDebug(ctx context.Context, msg string, args ...any) { +// WithAttrs 返回一个新的处理器,包含指定的属性 +func (h *ReadableTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + // 简化实现:不支持 With + return h +} + +// WithGroup 返回一个新的处理器,使用指定的组 +func (h *ReadableTextHandler) WithGroup(name string) slog.Handler { + // 简化实现:不支持组 + return h +} + +// checkAndRotateOnStartup 启动时检查日志文件是否需要按日期轮转 +func checkAndRotateOnStartup() error { + // 检查日志文件是否存在 + fileInfo, err := os.Stat(logFilePath) + if err != nil { + if os.IsNotExist(err) { + // 文件不存在,不需要轮转 + return nil + } + return fmt.Errorf("failed to stat log file: %w", err) + } + + // 获取文件的修改时间 + modTime := fileInfo.ModTime() + modDate := modTime.Format("2006-01-02") + today := time.Now().Format("2006-01-02") + + // 如果文件的日期和今天不同,进行轮转 + if modDate != today { + // 生成归档文件名(使用文件的修改日期) + timestamp := modTime.Format("2006-01-02-150405") + archivePath := filepath.Join(logDirPath, fmt.Sprintf("newapi.%s.log", timestamp)) + + // 重命名日志文件 + if err := os.Rename(logFilePath, archivePath); err != nil { + return fmt.Errorf("failed to archive old log file: %w", err) + } + + slog.Info("rotated old log file on startup", + "archive", archivePath, + "reason", "date changed") + + // 清理旧的日志文件 + gopool.Go(func() { + cleanOldLogFiles() + }) + } + + return nil +} + +// openLogFile 打开日志文件 +func openLogFile() error { + // 关闭旧的日志文件 + if logFile != nil { + logFile.Close() + } + + // 打开新的日志文件 + fd, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open log file: %w", err) + } + + logFile = fd + writeCount = 0 + return nil +} + +// rotateLogFile 轮转日志文件 +func rotateLogFile() error { + if logFile == nil { + return nil + } + + rotateCheckLock.Lock() + defer rotateCheckLock.Unlock() + + // 获取当前日志文件信息 + fileInfo, err := logFile.Stat() + if err != nil { + return fmt.Errorf("failed to stat log file: %w", err) + } + + // 检查文件大小是否需要轮转 + if fileInfo.Size() < maxLogSize { + return nil + } + + // 关闭当前日志文件 + logFile.Close() + + // 生成归档文件名 + timestamp := time.Now().Format("2006-01-02-150405") + archivePath := filepath.Join(logDirPath, fmt.Sprintf("newapi.%s.log", timestamp)) + + // 重命名当前日志文件为归档文件 + if err := os.Rename(logFilePath, archivePath); err != nil { + // 如果重命名失败,尝试复制 + if copyErr := copyFile(logFilePath, archivePath); copyErr != nil { + return fmt.Errorf("failed to archive log file: %w", err) + } + os.Truncate(logFilePath, 0) + } + + // 清理旧的日志文件 + gopool.Go(func() { + cleanOldLogFiles() + }) + + // 打开新的日志文件 + if err := openLogFile(); err != nil { + return err + } + + // 重新设置日志输出 + multiWriter := io.MultiWriter(os.Stdout, logFile) + gin.DefaultWriter = multiWriter + gin.DefaultErrorWriter = multiWriter + + handler := createHandler(multiWriter) + logMutex.Lock() + defaultLogger = slog.New(handler) + slog.SetDefault(defaultLogger) + logMutex.Unlock() + + slog.Info("log file rotated", + "reason", "size limit reached", + "archive", archivePath) + + return nil +} + +// cleanOldLogFiles 清理旧的日志文件 +func cleanOldLogFiles() { + if logDirPath == "" { + return + } + + files, err := os.ReadDir(logDirPath) + if err != nil { + slog.Error("failed to read log directory", "error", err) + return + } + + // 收集所有归档日志文件 + var logFiles []os.DirEntry + for _, file := range files { + if !file.IsDir() && strings.HasPrefix(file.Name(), "newapi.") && + strings.HasSuffix(file.Name(), ".log") && + file.Name() != defaultLogFileName { + logFiles = append(logFiles, file) + } + } + + // 如果归档文件数量超过限制,删除最旧的 + if len(logFiles) > maxLogFiles { + // 按名称排序(文件名包含时间戳) + sort.Slice(logFiles, func(i, j int) bool { + return logFiles[i].Name() < logFiles[j].Name() + }) + + // 删除最旧的文件 + deleteCount := len(logFiles) - maxLogFiles + for i := 0; i < deleteCount; i++ { + filePath := filepath.Join(logDirPath, logFiles[i].Name()) + if err := os.Remove(filePath); err != nil { + slog.Error("failed to remove old log file", + "file", filePath, + "error", err) + } else { + slog.Info("removed old log file", "file", logFiles[i].Name()) + } + } + } +} + +// copyFile 复制文件 +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + return err +} + +// getLogLevel 获取日志级别 +func getLogLevel() slog.Level { + // 支持环境变量配置 + if level := os.Getenv("LOG_LEVEL"); level != "" { + switch strings.ToUpper(level) { + case "DEBUG": + return slog.LevelDebug + case "INFO": + return slog.LevelInfo + case "WARN", "WARNING": + return slog.LevelWarn + case "ERROR": + return slog.LevelError + } + } + if common.DebugEnabled { - if len(args) > 0 { - msg = fmt.Sprintf(msg, args...) - } - logHelper(ctx, loggerDebug, msg) + return slog.LevelDebug + } + return slog.LevelInfo +} + +// getLogFormat 获取日志格式 +func getLogFormat() string { + if useJSONFormat { + return "json" + } + return "text" +} + +// checkAndRotateLog 检查并轮转日志 +func checkAndRotateLog() { + if logFile == nil { + return + } + + writeCount++ + if writeCount%checkRotateInterval == 0 { + gopool.Go(func() { + if err := rotateLogFile(); err != nil { + slog.Error("failed to rotate log file", "error", err) + } + }) } } -func logHelper(ctx context.Context, level string, msg string) { - writer := gin.DefaultErrorWriter - if level == loggerINFO { - writer = gin.DefaultWriter +// LogInfo 记录信息级别日志 +func LogInfo(ctx context.Context, msg string) { + if ctx == nil { + ctx = context.Background() + } + id := getRequestID(ctx) + logMutex.RLock() + logger := defaultLogger + logMutex.RUnlock() + logger.InfoContext(ctx, msg, "request_id", id) + checkAndRotateLog() +} + +// LogWarn 记录警告级别日志 +func LogWarn(ctx context.Context, msg string) { + if ctx == nil { + ctx = context.Background() + } + id := getRequestID(ctx) + logMutex.RLock() + logger := defaultLogger + logMutex.RUnlock() + logger.WarnContext(ctx, msg, "request_id", id) + checkAndRotateLog() +} + +// LogError 记录错误级别日志 +func LogError(ctx context.Context, msg string) { + if ctx == nil { + ctx = context.Background() + } + id := getRequestID(ctx) + logMutex.RLock() + logger := defaultLogger + logMutex.RUnlock() + logger.ErrorContext(ctx, msg, "request_id", id) + checkAndRotateLog() +} + +// LogSystemInfo 记录系统信息 +func LogSystemInfo(msg string) { + logMutex.RLock() + logger := defaultLogger + logMutex.RUnlock() + logger.Info(msg, "request_id", "SYSTEM") + checkAndRotateLog() +} + +// LogSystemError 记录系统错误 +func LogSystemError(msg string) { + logMutex.RLock() + logger := defaultLogger + logMutex.RUnlock() + logger.Error(msg, "request_id", "SYSTEM") + checkAndRotateLog() +} + +// LogDebug 记录调试级别日志 +func LogDebug(ctx context.Context, msg string, args ...any) { + if !common.DebugEnabled && getLogLevel() > slog.LevelDebug { + return + } + + if ctx == nil { + ctx = context.Background() + } + id := getRequestID(ctx) + if len(args) > 0 { + msg = fmt.Sprintf(msg, args...) + } + logMutex.RLock() + logger := defaultLogger + logMutex.RUnlock() + logger.DebugContext(ctx, msg, "request_id", id) + checkAndRotateLog() +} + +// getRequestID 从上下文中获取请求ID +func getRequestID(ctx context.Context) string { + if ctx == nil { + return "SYSTEM" } id := ctx.Value(common.RequestIdKey) if id == nil { - id = "SYSTEM" + return "SYSTEM" } - now := time.Now() - _, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg) - logCount++ // we don't need accurate count, so no lock here - if logCount > maxLogCount && !setupLogWorking { - logCount = 0 - setupLogWorking = true - gopool.Go(func() { - SetupLogger() - }) + if strID, ok := id.(string); ok { + return strID } + return "SYSTEM" } func LogQuota(quota int) string { diff --git a/main.go b/main.go index 481d0a600..5cd30bd70 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "bytes" "embed" "fmt" - "log" + "log/slog" "net/http" "os" "strconv" @@ -118,7 +118,9 @@ func main() { if os.Getenv("ENABLE_PPROF") == "true" { gopool.Go(func() { - log.Println(http.ListenAndServe("0.0.0.0:8005", nil)) + if err := http.ListenAndServe("0.0.0.0:8005", nil); err != nil { + slog.Error("pprof server failed", "error", err) + } }) go common.Monitor() common.SysLog("pprof enabled") @@ -127,7 +129,7 @@ func main() { // Initialize HTTP server server := gin.New() server.Use(gin.CustomRecovery(func(c *gin.Context, err any) { - common.SysLog(fmt.Sprintf("panic detected: %v", err)) + logger.LogSystemError(fmt.Sprintf("panic detected: %v", err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": gin.H{ "message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),