// Package service 提供日志轮转工具 package service import ( "fmt" "os" "path/filepath" "strconv" "sync" ) // RotateConfig 配置日志轮转参数 type RotateConfig struct { MaxSize int64 // 单个日志文件最大字节数 MaxFiles int // 最多保留的轮转文件数 } // DefaultRotateConfig 从环境变量读取轮转配置 func DefaultRotateConfig(envPrefix string, defaultMaxSize int64, defaultMaxFiles int) RotateConfig { cfg := RotateConfig{ MaxSize: defaultMaxSize, MaxFiles: defaultMaxFiles, } if v := os.Getenv(envPrefix + "_MAX_SIZE"); v != "" { if n, err := parseSize(v); err == nil && n > 0 { cfg.MaxSize = n } } if v := os.Getenv(envPrefix + "_MAX_FILES"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { cfg.MaxFiles = n } } return cfg } // parseSize 解析带单位的字节数(如 "50MB", "20M", "1048576") func parseSize(s string) (int64, error) { suffixes := map[string]int64{ "KB": 1024, "MB": 1024 * 1024, "GB": 1024 * 1024 * 1024, "K": 1024, "M": 1024 * 1024, "G": 1024 * 1024 * 1024, } for suffix, mult := range suffixes { if len(s) > len(suffix) && s[len(s)-len(suffix):] == suffix { n, err := strconv.ParseInt(s[:len(s)-len(suffix)], 10, 64) if err != nil { return 0, err } return n * mult, nil } } return strconv.ParseInt(s, 10, 64) } // RotateIfNeeded 检查日志文件大小并在需要时轮转 func RotateIfNeeded(logPath string, cfg RotateConfig) error { info, err := os.Stat(logPath) if err != nil { if os.IsNotExist(err) { return nil } return err } if info.Size() < cfg.MaxSize { return nil } return rotateFiles(logPath, cfg.MaxFiles) } // rotateFiles 执行日志轮转:logPath -> logPath.1, logPath.N-1 -> delete func rotateFiles(logPath string, maxFiles int) error { // 删除最旧的文件 oldest := fmt.Sprintf("%s.%d", logPath, maxFiles) os.Remove(oldest) // 忽略不存在的错误 // 逐个重命名:N-1 -> N, N-2 -> N-1, ... 1 -> 2 for i := maxFiles - 1; i >= 1; i-- { src := fmt.Sprintf("%s.%d", logPath, i) dst := fmt.Sprintf("%s.%d", logPath, i+1) if _, err := os.Stat(src); err == nil { os.Rename(src, dst) } } // 当前文件 -> .1 if _, err := os.Stat(logPath); err == nil { return os.Rename(logPath, logPath+".1") } return nil } // RotatingWriter 是一个带轮转功能的 io.Writer type RotatingWriter struct { mu sync.Mutex path string cfg RotateConfig f *os.File } // NewRotatingWriter 创建一个带轮转功能的写入器 func NewRotatingWriter(path string, cfg RotateConfig) (*RotatingWriter, error) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return nil, err } rw := &RotatingWriter{path: path, cfg: cfg} if err := rw.open(); err != nil { return nil, err } return rw, nil } func (w *RotatingWriter) open() error { // 先尝试轮转 RotateIfNeeded(w.path, w.cfg) f, err := os.OpenFile(w.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return err } w.f = f return nil } func (w *RotatingWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() n, err := w.f.Write(p) if err != nil { return n, err } // 每次写入后检查大小(懒轮转) if info, e := w.f.Stat(); e == nil && info.Size() >= w.cfg.MaxSize { w.f.Close() w.open() } return n, nil } func (w *RotatingWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.f != nil { return w.f.Close() } return nil }