package notify import ( "encoding/json" "os" "path/filepath" "time" ) // alertState 记录告警节流信息。 // // 为什么必须落盘:进程检测到挂载失效后会主动退出、由 restart 策略拉起。 // 若节流时间只存在内存里,重启后 lastAlert 归零,会立刻再次告警 —— // 持续故障就变成「每轮重启都发一条飞书」,把真正的告警淹成骚扰。 type alertState struct { // LastDegradedAlert 是上次发送「归档目录不可用」告警的时间。 LastDegradedAlert time.Time `json:"lastDegradedAlert,omitempty"` // LastDegradedReason 用于在故障原因变化时立即重新告警。 LastDegradedReason string `json:"lastDegradedReason,omitempty"` // ExitCount 统计因挂载失效而退出的累计次数,用于识别「反复重启」。 ExitCount int `json:"exitCount,omitempty"` } func alertStatePath(stateDir string) string { return filepath.Join(stateDir, "alert-state.json") } func loadAlertState(stateDir string) alertState { var st alertState if stateDir == "" { return st } body, err := os.ReadFile(alertStatePath(stateDir)) if err != nil { return st } _ = json.Unmarshal(body, &st) return st } func saveAlertState(stateDir string, st alertState) error { if stateDir == "" { return nil } path := alertStatePath(stateDir) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return err } body, err := json.MarshalIndent(st, "", " ") if err != nil { return err } tmp := path + ".tmp" if err := os.WriteFile(tmp, body, 0o600); err != nil { return err } return os.Rename(tmp, path) } // shouldAlert 判断是否该发告警:间隔到了,或故障原因发生变化。 func (st alertState) shouldAlert(reason string, now time.Time, interval time.Duration) bool { if st.LastDegradedAlert.IsZero() { return true } if reason != st.LastDegradedReason { // 原因变了(例如从「目录不存在」变成「不可读」)值得立刻提醒。 return true } return now.Sub(st.LastDegradedAlert) >= interval }