package logging import ( "compress/gzip" "fmt" "io" "os" "path/filepath" "strings" "sync" "time" ) type dailyWriter struct { mu sync.Mutex dir string prefix string location *time.Location retainDays int day string file *os.File } func newDailyWriter(dir string, prefix string, location *time.Location, retainDays int) *dailyWriter { if retainDays < 0 { retainDays = 0 } return &dailyWriter{ dir: dir, prefix: prefix, location: location, retainDays: retainDays, } } func (w *dailyWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if err := w.rotateIfNeeded(time.Now().In(w.location)); err != nil { return 0, err } return w.file.Write(p) } func (w *dailyWriter) Sync() error { w.mu.Lock() defer w.mu.Unlock() if w.file == nil { return nil } return w.file.Sync() } func (w *dailyWriter) rotateIfNeeded(now time.Time) error { day := now.Format("2006-01-02") if w.file != nil && w.day == day { return nil } if err := os.MkdirAll(w.dir, 0o755); err != nil { return err } prevDay := w.day if w.file != nil { _ = w.file.Close() w.file = nil } path := filepath.Join(w.dir, fmt.Sprintf("%s-%s.log", w.prefix, day)) file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return err } w.file = file w.day = day // 异步压缩昨日日志并清理过期文件,避免阻塞业务写路径 if prevDay != "" && prevDay != day { go w.maintain(prevDay, now) } else { // 进程启动或首写:尝试压缩昨天 + 清理过期 yesterday := now.AddDate(0, 0, -1).Format("2006-01-02") go w.maintain(yesterday, now) } return nil } func (w *dailyWriter) maintain(prevDay string, now time.Time) { if prevDay != "" { _ = w.compressDay(prevDay) } if w.retainDays > 0 { _ = w.purgeOlderThan(now) } } func (w *dailyWriter) compressDay(day string) error { srcPath := filepath.Join(w.dir, fmt.Sprintf("%s-%s.log", w.prefix, day)) dstPath := srcPath + ".gz" if _, err := os.Stat(dstPath); err == nil { // 已压缩则删除明文(若仍存在) _ = os.Remove(srcPath) return nil } src, err := os.Open(srcPath) if err != nil { if os.IsNotExist(err) { return nil } return err } defer src.Close() dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) if err != nil { return err } gz := gzip.NewWriter(dst) if _, err := io.Copy(gz, src); err != nil { _ = gz.Close() _ = dst.Close() _ = os.Remove(dstPath) return err } if err := gz.Close(); err != nil { _ = dst.Close() _ = os.Remove(dstPath) return err } if err := dst.Close(); err != nil { return err } return os.Remove(srcPath) } func (w *dailyWriter) purgeOlderThan(now time.Time) error { // 按「日历天」比较:保留最近 retainDays 天(含当天),更早的 .log / .log.gz 删除。 today := time.Date(now.In(w.location).Year(), now.In(w.location).Month(), now.In(w.location).Day(), 0, 0, 0, 0, w.location) cutoff := today.AddDate(0, 0, -w.retainDays) // day < cutoff 才删除;retainDays=2 且今天 12 号 → 删除 10 号之前 entries, err := os.ReadDir(w.dir) if err != nil { return err } prefix := w.prefix + "-" for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() if !strings.HasPrefix(name, prefix) { continue } // app-2026-07-11.log 或 app-2026-07-11.log.gz rest := strings.TrimPrefix(name, prefix) rest = strings.TrimSuffix(rest, ".gz") rest = strings.TrimSuffix(rest, ".log") day, err := time.ParseInLocation("2006-01-02", rest, w.location) if err != nil { continue } if day.Before(cutoff) { _ = os.Remove(filepath.Join(w.dir, name)) } } return nil }