package logging import ( "compress/gzip" "errors" "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 (w *dailyWriter) Open() error { w.mu.Lock() defer w.mu.Unlock() return w.rotateIfNeeded(time.Now().In(w.location)) } 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, 0o700); err != nil { return err } if err := os.Chmod(w.dir, 0o700); err != nil { return err } if w.file != nil { if err := w.file.Close(); err != nil { return err } 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, 0o600) if err != nil { return err } if err := file.Chmod(0o600); err != nil { _ = file.Close() return err } w.file = file w.day = day // 异步补压缩全部历史日志并清理过期文件,避免漏掉停机期间的日期。 go w.maintain(now) return nil } func (w *dailyWriter) maintain(now time.Time) { if err := w.compressHistorical(now); err != nil { writeMaintenanceError(err) } if w.retainDays > 0 { if err := w.purgeOlderThan(now); err != nil { writeMaintenanceError(err) } } } func (w *dailyWriter) compressHistorical(now time.Time) error { entries, err := os.ReadDir(w.dir) if err != nil { return err } today := now.In(w.location).Format("2006-01-02") for _, entry := range entries { day, ok := w.dayFromFilename(entry.Name(), ".log") if !ok || day >= today { continue } if err := w.compressDay(day); err != nil { return fmt.Errorf("压缩 %s 日志失败: %w", day, err) } } return nil } 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 { if err := validateGzip(dstPath); err == nil { if err := os.Remove(srcPath); err != nil && !os.IsNotExist(err) { return err } return nil } if err := os.Remove(dstPath); err != nil { return err } } src, err := os.Open(srcPath) if err != nil { if os.IsNotExist(err) { return nil } return err } defer src.Close() dst, err := os.CreateTemp(w.dir, "."+filepath.Base(dstPath)+".tmp-*") if err != nil { return err } tmpPath := dst.Name() defer func() { _ = os.Remove(tmpPath) }() if err := dst.Chmod(0o600); err != nil { _ = dst.Close() return err } gz := gzip.NewWriter(dst) if _, err := io.Copy(gz, src); err != nil { _ = gz.Close() _ = dst.Close() return err } if err := gz.Close(); err != nil { _ = dst.Close() return err } if err := dst.Sync(); err != nil { _ = dst.Close() return err } if err := dst.Close(); err != nil { return err } if err := os.Rename(tmpPath, dstPath); err != nil { return err } return os.Remove(srcPath) } func validateGzip(path string) error { file, err := os.Open(path) if err != nil { return err } defer file.Close() reader, err := gzip.NewReader(file) if err != nil { return err } _, copyErr := io.Copy(io.Discard, reader) closeErr := reader.Close() return errors.Join(copyErr, closeErr) } 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 - 1)) 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) || (!strings.HasSuffix(name, ".log") && !strings.HasSuffix(name, ".log.gz")) { continue } 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) { if err := os.Remove(filepath.Join(w.dir, name)); err != nil && !os.IsNotExist(err) { return err } } } return nil } func (w *dailyWriter) dayFromFilename(name string, suffix string) (string, bool) { prefix := w.prefix + "-" if !strings.HasPrefix(name, prefix) || !strings.HasSuffix(name, suffix) { return "", false } day := strings.TrimSuffix(strings.TrimPrefix(name, prefix), suffix) if _, err := time.ParseInLocation("2006-01-02", day, w.location); err != nil { return "", false } return day, true } func writeMaintenanceError(err error) { _, _ = fmt.Fprintf(os.Stderr, "%s | ERROR | 日志维护失败 | error=%q\n", time.Now().Format("2006-01-02 15:04:05.000"), err.Error()) }