package loggerx import ( "fmt" "os" "path/filepath" "strings" "time" ) // 监听dir文件夹的所有文件,循环查找是否有超过days天的文件,删除掉 func (l *Logger) delete() { tick := time.NewTicker(time.Hour) defer tick.Stop() for { select { case <-tick.C: // 后台清理不能把整个进程带崩 func() { defer func() { if r := recover(); r != nil { fmt.Println("清理日志文件异常:", r) } }() // 先清掉「压缩完成但源文件没删掉」留下的冗余文件 if err := l.healCompressedLeftovers(); err != nil { l.reportError(err) } // 清掉崩溃/被杀留下的半截压缩临时文件 if err := l.sweepStaleTemps(); err != nil { l.reportError(err) } // 释放长时间不活跃的句柄(通道名多变的场景会积累大量 fd + 32KB 缓冲) if err := l.sweepIdleHandles(); err != nil { l.reportError(err) } if err := l.walkAndDel(); err != nil { l.reportError(err) } }() case <-l.ctx.Done(): return case <-l.done: return } } } func (l *Logger) walkAndDel() error { // 没设置删除天数(<=0 表示不删除) if l.option.days <= 0 { return nil } deadline := time.Now().AddDate(0, 0, -l.option.days) live := l.livePaths() err := filepath.Walk(l.option.dir, func(path string, info os.FileInfo, err error) error { if err != nil { // 单个文件出错不应该中断整个遍历 l.reportError(err) return nil } // 最后修改时间在保留期内,跳过 if info.ModTime().After(deadline) { return nil } if info.IsDir() { if !isEmptyDir(path) { return nil } } else if ext := filepath.Ext(path); ext != ".log" && ext != ".gz" { // 只清理日志与压缩归档 return nil } // 绝不能删正在被本实例写入的文件: // Linux 上删除已打开的文件会成功,之后所有写入都进了「已 unlink 的 inode」, // 日志对外彻底不可见、空间也不释放;Windows 上则会一直删不掉刷报错 if live[path] { return nil } // 删除文件 / 空目录 if rerr := os.Remove(path); rerr != nil { // 正被外部进程占用的文件删不掉,等下一轮再试,不要终止遍历 l.reportError(fmt.Errorf("loggerx: 删除文件失败 %s: %w", path, rerr)) return nil } return nil }) return err } // livePaths 当前所有打开着的日志文件路径集合 func (l *Logger) livePaths() map[string]bool { l.mu.RLock() defer l.mu.RUnlock() live := make(map[string]bool, len(l.filePath)) for _, f := range l.filePath { if f != nil && f.fileName != "" { live[f.fileName] = true } } return live } // staleTempAge 多久没动静的临时文件算垃圾 const staleTempAge = time.Hour // sweepStaleTemps 清理崩溃/被杀时留下的半截压缩临时文件 // // gzipFile 只在自己的错误分支里删 .gz.tmp,进程被 SIGKILL 时来不及删, // 而这些文件的扩展名既不是 .log 也不是 .gz,原来的清理逻辑完全看不到它们 func (l *Logger) sweepStaleTemps() error { cutoff := time.Now().Add(-staleTempAge) var errs []error _ = filepath.Walk(l.option.dir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } if !strings.HasSuffix(path, gzipTempSuffix) { return nil } // 留一点余量:可能正好有一个压缩任务在写它 if info.ModTime().After(cutoff) { return nil } if rerr := os.Remove(path); rerr != nil { errs = append(errs, fmt.Errorf("loggerx: 清理半截压缩文件失败 %s: %w", path, rerr)) return nil } return nil }) return joinErrors(errs) } // 判断空文件夹 func isEmptyDir(path string) bool { files, err := os.ReadDir(path) if err != nil { fmt.Println(err) return false } return len(files) == 0 // 文件夹为空 }