fix(alerts,analysis): 告警引擎稳定性 5 项修复;每日量接口去掉同步 Loki 查询(20s→10ms)

=== 一、告警引擎稳定性审查(引擎本身是异步的:ticker + goroutine)===
1) 【高】ticker 无重叠保护
   ticker 每 1 分钟触发;一轮规则检查 8 条 × 20~30s = 2~4 分钟,远超一分钟。
   原来每过一分钟就再开一轮 → 查询越堆越多,抢走页面查询的 Loki 资源,
   每轮越来越慢,最终表现为"告警不响了"。
   → 新增 alertCheckRunning(atomic.Bool),上一轮未完成直接跳过本轮并记日志。

2) 【高】单条规则无超时
   RunAlertCheck(context.Background()) 没有 deadline:Loki 一旦挂住,整轮永久运行,
   叠加问题 1 会让引擎再也不启动新一轮(静默死亡)。
   → 每轮总超时 5 分钟(alertRoundTimeout)+ 单条规则 90s(alertRuleTimeout)。

3) 【中】panic 只在整个 tick 层 recover
   一条规则 panic 会让本轮后续规则全部跳过。
   → per-rule recover,panic 记为一条 failed 投递记录,后续规则继续。

4) 【中】冷却记录每分钟写一条,把真实发送记录挤光
   AlertDeliveries 只保留最近 50 条,而处于冷却期时每个 tick 都写一条 cooldown,
   一小时内真实的"已发送"记录就全被挤掉了,发送历史没法看。
   → recordCooldownThrottled():同一规则 10 分钟最多写一条。附单测。

5) 规则串行本身保留(避免同时压垮 Loki),但配合 1) 才安全。

=== 二、每日日志量接口:20 秒 → 10 毫秒 ===
根因:GetDailyVolumeHistory 里**同步** await 一次"整天 index/stats"补抓当天数据,
那次查询超时 20s → 整个接口 20s → 前端超时 → 页面显示"每日日志量没数据";
更糟的是这条重查询还和页面的 histogram/stats 抢 Loki,导致那两个接口同时 502
(访问日志实证:daily-volume latency_ms 20000.997,stats/histogram 502)。
→ 接口改为**只读库**,不触发任何 Loki 查询;当天数据由后台每 30 分钟的
  定时任务补(CaptureRecentDailyVolumes)。
→ 同时把整天 index/stats 的超时从 20s 提到 90s(370GB/天,20s 不够)。
实测:10ms。

=== 三、通知通道改名「默认告警」(schemaVersion v8→v9) ===
"lark 默认告警" / "默认通知通道(待配置 Webhook)" → 统一为「默认告警」。
只改名,不动 Webhook、不动 enabled:
首次部署创建的占位通道必须保持「空 Webhook + 禁用」,由管理员自己填,
否则会出现"已启用但空地址"的通道,告警一命中就全部投递失败。

=== 四、其它文案 ===
「每日日志量」副标题精简为「每天的日志总量,平台保留历史趋势」,
并移除「平台留存历史」徽标(用户反馈多余)。

=== 实测(303134c0c718)===
  /logs/daily-volume:10ms(原 20000ms)
  通道列表:id=1 name="默认告警" enabled=true hasWebhook=true
  go test ./... 全绿(含新增 TestRecordCooldownThrottled)
This commit is contained in:
opsx committed 2026-10-29 03:38:30 +00:00
1 parent 607b67a2ed
commit cb1ef1dff4
9 files changed
+149 -36

No files matched your search

+28 -1
View File
@@ -114,6 +114,27 @@ func (d *JsonDB) raiseLegacyAlertThresholds() {
}
}
// normalizeDefaultChannelName 把默认通道的名字统一成「默认告警」。
//
// 只改名,不动 Webhook 与 enabled:首次部署的占位通道必须保持"空地址 + 禁用",
// 让管理员自己填(避免填了个空地址的启用通道,告警一命中就全部投递失败)。
func (d *JsonDB) normalizeDefaultChannelName() {
const canonical = "默认告警"
legacyNames := map[string]bool{
"lark 默认告警": true,
"默认通知通道(待配置 Webhook)": true,
"默认通知通道": true,
}
for i := range d.NotifyChannels {
ch := &d.NotifyChannels[i]
if legacyNames[ch.Name] {
log.Printf("[schema] 通知通道 %q 改名为 %q", ch.Name, canonical)
ch.Name = canonical
ch.UpdatedAt = time.Now()
}
}
}
// removeOrphanManagedLogSources 清掉"部署已不存在、但数据源还在"的孤儿记录。
//
// 判据:Managed=true(平台托管)且没有任何同名部署。
@@ -409,7 +430,7 @@ func (d *JsonDB) dedupeLogSourceIDs() {
//
// v0/v1(无字段)→ 1:新增 auditLogs / schemaVersion(老库走零值兜底即可,无需处理)
// v1 → 2:预留示例(如把 ScrapeJobs 从 logPath 拆分时,需按旧结构生成默认 job)
const CurrentSchemaVersion = 8
const CurrentSchemaVersion = 9
// migrateSchema 按版本号顺序迁移老库。返回是否发生了迁移(需要回写)。
func (d *JsonDB) migrateSchema() bool {
@@ -480,6 +501,12 @@ func (d *JsonDB) migrateSchema() bool {
// 500~5000 这一档实测在业务高峰是常态量,贴着基线就等于天天报。
d.raiseLegacyAlertThresholds()
d.SchemaVersion = 8
case 8:
// v8 → v9:通知通道统一改名为「默认告警」。
// 首次部署创建的占位通道保持**禁用 + 空 Webhook**(由管理员自己填),
// 这里只改名字,不动 Webhook、不动启用状态。
d.normalizeDefaultChannelName()
d.SchemaVersion = 9
default:
// 防御:出现未知版本(比当前代码还新)时不乱动,避免降级把数据写坏
log.Printf("[schema] 数据库版本 %d 高于当前代码支持版本 %d,跳过迁移", d.SchemaVersion, CurrentSchemaVersion)