package logging import ( "fmt" "os" "path/filepath" "sort" "strconv" "strings" "sync" "time" ) // rotator 是「按天 + 按大小」轮转的文件写入器,并按份数清理旧文件。 // // 现场要跑一整天,日志既不能无限涨到把盘写满,也不能因为写不进去就影响业务: // 一旦写盘失败就永久降级(只通知一次),后续日志由控制台那条链路兜住。 type rotator struct { dir string prefix string maxBytes int64 keep int // onError 只在第一次写盘失败时回调一次:每条日志都刷告警会把控制台淹掉。 onError func(error) mu sync.Mutex file *os.File size int64 curName string curDay string curIdx int broken bool } func newRotator(dir, prefix string, maxBytes int64, keep int, onError func(error)) *rotator { return &rotator{dir: dir, prefix: prefix, maxBytes: maxBytes, keep: keep, onError: onError} } // Write 永不返回错误:日志写不进去是运维问题,不能变成业务请求失败。 func (w *rotator) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if w.broken { return len(p), nil } if err := w.prepare(len(p), time.Now()); err != nil { w.degrade(err) return len(p), nil } n, err := w.file.Write(p) w.size += int64(n) if err != nil { w.degrade(err) } return len(p), nil } // Close 关掉当前文件;进程退出前调用,保证缓冲里的内容不会丢。 func (w *rotator) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.file == nil { return nil } err := w.file.Close() w.file = nil return err } func (w *rotator) degrade(err error) { w.broken = true if w.file != nil { _ = w.file.Close() w.file = nil } if w.onError != nil { w.onError(err) } } // prepare 保证当前打开的是「今天该写、且还装得下」的那个文件。 func (w *rotator) prepare(need int, now time.Time) error { day := now.Format("20060102") if w.file == nil || day != w.curDay { if err := w.closeLocked(); err != nil { return err } w.curDay = day w.curIdx = 0 return w.openLocked() } // 当前文件已经装了东西且放不下这一条时才轮转:空文件永远装得下, // 否则超长单条会把每个新文件立刻撑爆、无限轮转。 if w.size > 0 && w.size+int64(need) > w.maxBytes { if err := w.closeLocked(); err != nil { return err } w.curIdx++ return w.openLocked() } return nil } func (w *rotator) closeLocked() error { if w.file == nil { return nil } err := w.file.Close() w.file = nil return err } func (w *rotator) openLocked() error { if err := os.MkdirAll(w.dir, 0o755); err != nil { return err } name := w.fileName(w.curDay, w.curIdx) f, err := os.OpenFile(filepath.Join(w.dir, name), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return err } info, err := f.Stat() if err != nil { _ = f.Close() return err } w.file = f w.size = info.Size() w.curName = name w.prune() return nil } // fileName 是「<类别>-.log」,同一天的第 N 个续写文件加序号: // operations-20260911.log / operations-20260911.1.log / .2.log… func (w *rotator) fileName(day string, idx int) string { if idx <= 0 { return fmt.Sprintf("%s-%s.log", w.prefix, day) } return fmt.Sprintf("%s-%s.%d.log", w.prefix, day, idx) } // prune 清掉超出保留份数的最旧文件;调用方必须持有锁。 // 轮转与开新文件时都会走一遍,保留策略才有执行点(只在启动时清会一直涨)。 func (w *rotator) prune() { if w.keep <= 0 { return } files := w.list() if len(files) <= w.keep { return } for _, f := range files[:len(files)-w.keep] { if f.name == w.curName { continue } _ = os.Remove(filepath.Join(w.dir, f.name)) } } type logFile struct { name string day string index int } // list 列出本类别的日志文件,按「日期 + 序号」从旧到新排序。 // 只认自己写出来的命名,别人的文件一律不碰。 func (w *rotator) list() []logFile { entries, err := os.ReadDir(w.dir) if err != nil { return nil } prefix := w.prefix + "-" out := make([]logFile, 0, len(entries)) for _, e := range entries { if e.IsDir() { continue } name := e.Name() if !strings.HasPrefix(name, prefix) || !strings.HasSuffix(name, ".log") { continue } rest := strings.TrimSuffix(strings.TrimPrefix(name, prefix), ".log") day, idxText, _ := strings.Cut(rest, ".") if len(day) != 8 { continue } idx := 0 if idxText != "" { n, err := strconv.Atoi(idxText) if err != nil { continue } idx = n } out = append(out, logFile{name: name, day: day, index: idx}) } sort.Slice(out, func(i, j int) bool { if out[i].day != out[j].day { return out[i].day < out[j].day } return out[i].index < out[j].index }) return out }