package services import ( "errors" "strconv" "time" "lokistack/models" "lokistack/repository" ) // 平台自身数据的保留期设置(不是 Loki 日志的保留期)。 // // 为什么需要可配:不同团队对"告警发送历史留多久 / 审计日志留多久"要求不同 — // 合规场景要 90~180 天,小团队 7 天就够。写死在代码里只能改一次发一次版。 // // 注意区分:Loki 的日志保留期是**部署级**配置(组件部署 → 保留策略), // 这里管的是平台自己产生的小数据(投递历史、审计日志)。 const ( settingKeyAlertDeliveryDays = "alertDeliveryRetentionDays" settingKeyAuditLogDays = "auditLogRetentionDays" defaultAlertDeliveryDays = 7 defaultAuditLogDays = 90 // 硬上限:即使配置得很大,也不能让 JSON 文件型 DB 无界增长 maxAlertDeliveries = 20000 ) // RetentionSettings 平台数据保留期(天) type RetentionSettings struct { AlertDeliveryDays int `json:"alertDeliveryDays"` AuditLogDays int `json:"auditLogDays"` } // GetRetentionSettings 读取保留期配置,未配置时返回默认值。 func GetRetentionSettings() RetentionSettings { db := repository.GetDB() if db == nil { return retentionDefaults() } db.RLock() defer db.RUnlock() return retentionFromDBLocked(db) } func retentionDefaults() RetentionSettings { return RetentionSettings{ AlertDeliveryDays: defaultAlertDeliveryDays, AuditLogDays: defaultAuditLogDays, } } // retentionFromDBLocked 从已持有的锁内读取配置,**不会再加锁**。 // // 为什么必须单独一个函数:Go 的 sync.RWMutex 不可重入。 // 调用方已经 db.Lock() 时再调 GetRetentionSettings()(内部 RLock)会**永久死锁** —— // 实测把整个平台写路径卡死(登录都要写审计,于是登录 28 秒返回空)。 func retentionFromDBLocked(db *repository.JsonDB) RetentionSettings { out := retentionDefaults() for _, item := range db.Settings { switch item.Key { case settingKeyAlertDeliveryDays: if v, err := strconv.Atoi(item.Value); err == nil && v > 0 { out.AlertDeliveryDays = v } case settingKeyAuditLogDays: if v, err := strconv.Atoi(item.Value); err == nil && v > 0 { out.AuditLogDays = v } } } return out } // SaveRetentionSettings 保存保留期配置(天数)。 func SaveRetentionSettings(alertDays, auditDays int) error { if alertDays < 1 || alertDays > 365 { return errors.New("告警发送历史保留天数需在 1~365 之间") } if auditDays < 1 || auditDays > 3650 { return errors.New("审计日志保留天数需在 1~3650 之间") } db := repository.GetDB() if db == nil { return errors.New("数据库未初始化") } db.Lock() upsertSettingLocked(db, settingKeyAlertDeliveryDays, strconv.Itoa(alertDays)) upsertSettingLocked(db, settingKeyAuditLogDays, strconv.Itoa(auditDays)) db.RequestSave() db.Unlock() // 立刻按新策略裁剪一次,避免"改了配置但旧数据还在" PruneAlertDeliveries() PruneAuditLogs() return nil } // upsertSettingLocked 写入/更新一条设置(调用方必须已持写锁)。 func upsertSettingLocked(db *repository.JsonDB, key, value string) { for i := range db.Settings { if db.Settings[i].Key == key { db.Settings[i].Value = value return } } // 不能用 db.NextID():它内部会 d.mu.Lock(),而调用方已经持有写锁, // sync.Mutex 不可重入 → 永久死锁(这个坑在审计日志里踩过一次)。 // 这里在锁内自己算最大 ID + 1。 nextID := uint(1) for _, item := range db.Settings { if item.ID >= nextID { nextID = item.ID + 1 } } db.Settings = append(db.Settings, models.SystemSetting{ ID: nextID, Key: key, Value: value, }) } // alertDeliveryRetentionLocked 返回投递历史保留时长(调用方必须已持锁)。 func alertDeliveryRetentionLocked(db *repository.JsonDB) time.Duration { return time.Duration(retentionFromDBLocked(db).AlertDeliveryDays) * 24 * time.Hour } // auditLogRetentionLocked 返回审计日志保留时长(调用方必须已持锁)。 func auditLogRetentionLocked(db *repository.JsonDB) time.Duration { return time.Duration(retentionFromDBLocked(db).AuditLogDays) * 24 * time.Hour }