告警规则任务级化:从「全类型共享一条规则」到「每个任务各管各的」
导读
给 AI 告警弹窗加上接收人定向后,暴露出一个结构性问题:告警规则是「类型级」共享的,同类型的所有监控任务共用一条规则,A 任务改接收人会把 B 任务的也改掉。这次把规则挂到任务级,语义是「任务优先、共享兜底」。改造中确认了三条反直觉的约束(禁用不回退、只认 ‘0’、共享查询显式隔离),顺手修了一个 threshold_status 整列写不进库的存量 bug,全部由单测 + 真实 SQL + 接口级 E2E 验证。
🎧 文章导读
🎵 背景音乐
问题:规则是「类型级」共享的
承接上一篇 AI 告警 WebSocket 弹窗。8/5 给弹窗加了接收人定向后发现一个结构性问题:告警规则是类型级共享的——所有 bizType + alarmType 相同的监控任务,共用 alarming_processor 表里的同一条规则。
这意味着等级、阈值、广播、显示屏、工单、弹窗接收人全部共享。A 任务的运营人员把接收人改成自己,B 任务的告警也跟着改道。原型设计上每个监控任务有一页自己的规则设置,数据模型却没跟上。
旧链路里 AlarmingEvenSinkServiceImpl.generalAlarmProcessing 按 bizType + alarmType + configStatus=开 查出那条共享规则,一路用到尾。
目标:任务优先、共享兜底
每个监控任务可以拥有自己的规则。匹配语义:
- 事件带了任务 ID 且任务配了规则 → 用任务规则。
- 任务没配规则 → 回退共享规则,行为与旧链路完全一致。存量共享规则自动成为兜底,门禁、消防这类无任务事件不受影响。
这个兜底设计让改造可以灰度:不配新规则,系统就是旧系统。
核心匹配语义:三条反直觉约束
resolveProcessor 的决策树(三轮评审确认):
1 | flowchart TD |
三个看起来别扭、但每一条都有实证的设计:
1. 禁用不回退。 任务规则已禁用 ≠ 没配规则。如果禁用就回退共享规则,运维以为「关掉这个任务的告警」,实际告警被共享规则推给了另一批人。所以 findByMonitoringId 故意不预过滤 configStatus——不过滤才能区分「不存在」和「已禁用」。
2. 严格只认 '0'。 与旧运行时语义一致:旧代码查询时就带 configStatus='0' 条件,其他值从来不命中。如果改成「只拦截 ‘1’」,反而比旧语义宽松,会把历史上从来不算数的 null/其他值激活。
3. 共享查询必须显式 monitoring_id IS NULL。 框架的 findOneByCondition 是 list.get(0) 无序取首条。不加隔离,表里有了任务规则之后,「查共享规则」可能取到某条任务规则。
改动清单
数据库(building_biz)
迁移 V20260806.12.42__alter_alarming_processor_add_monitoring_id.sql:alarming_processor 加 monitoring_id varchar(64) NULL + 唯一索引。
利用了 MySQL 唯一索引允许多个 NULL 的特性:每个任务至多一条规则,共享规则(NULL)数量不受限,存量数据不用刷。
代码
| 模块 | 文件 | 改动 |
|---|---|---|
| warning | AlarmingProcessorMapper.xml |
monitoring_id 补 7 处映射;新增 findByMonitoringId(带 delete_flag、不带状态)、findEnabledSharedProcessor(IS NULL + 类型 + 开)、findSharedProcessor(不看状态,判重用);listByPage 加 <choose> 隔离 |
| warning | AlarmingProcessorServiceImpl |
新增 resolveProcessor(上面的决策树,命中后走 processingDetails 加载广播/显示屏/流程关联);addSelective 判重按任务/共享分流;新增 saveTaskProcessor 聚合保存 |
| warning | AlarmingEvenSinkServiceImpl |
原查询块换成 resolveProcessor(eventData) 一行 |
| warning | AlarmingProcessorController |
新增 POST /alarmingProcessor/saveTaskProcessor |
| security | MonitoringResultPictureServiceImpl |
越界/黑名单两处事件 setMonitoringId(任务ID);响应处理拆分「业务失败」(log.error)与「规则未配置/已禁用」(log.info,正常分支) |
| service-model | AlarmingProcessor / AlarmingEventData |
各加一个 monitoringId 字段 |
saveTaskProcessor 是聚合保存:主表 upsert + 关联配置全量替换,单事务,幂等。前端一包提交,失败重试直接覆盖,不产生半成品。
坑一:OR 没括号,IS NULL 被短路
既有 XML 里列表查询带着 AND biz_type = ? OR alarm_type = ?,没加括号。往这个查询里追加 monitoring_id IS NULL 隔离时,隔离条件会被 OR 短路:
1 | -- 修复前(示意):AND 优先级高于 OR |
这不是读代码看出来的,是真实 SQL A/B 验证实测出来的:无括号版漏 2 行任务规则进列表,括号版干净。改造老 SQL 时,先跑一遍再信自己的眼睛。
坑二(顺带修复):threshold_status 整列写不进库
测试过程中发现阈值开关怎么配都不生效,追到底是个存量 bug:AlarmingProcessorMapper.xml 的 add / addSelective / update / updateSelective 四个写语句全部没有 threshold_status 映射——resultMap 和查询条件里有,写入语句里没有。
后果链条:阈值开关永远 null → thresholdProcessing 不短路 → 要求 alarmThreshold + thresholdRule 非空 → 校验不过 → 不产生告警。老接口流程同样中招,只是之前没人配阈值,一直没暴露。四个语句补上映射后修复。
测试验证
- 单测:warning 15/15、security 34/34(
mvn clean test,JDK 8)。 - 真实 SQL:building_biz 造三类数据,验证 IS NULL 隔离、按任务查询、唯一索引(Duplicate entry 复现)、count 隔离、OR 括号 A/B。
- 接口级 E2E:gateway → warning 直连,用
inputIteration模拟 security 上报事件。saveTaskProcessor建规则 ✅(park_id 自动注入正确);事件命中任务规则走到 pushAlarmPopup ✅;禁用不回退、回退兜底、老链路、类型不匹配等场景全部通过 ✅。
E2E 的环境坑比用例本身更费时间,单独沉淀了一篇配方:本地接口级 E2E 测试配方。
前端交接:任何时刻保证「规则 ⊇ 任务」
顺序原则一句话:危险态是「任务存在而规则缺失」(会回退共享规则、把告警推错人),安全态是「规则存在而任务缺失」(规则惰性躺尸,无害)。所以任何时刻保证规则集合 ⊇ 任务集合。
- 保存:前端生成任务 UUID → ①
saveTaskProcessor建规则(monitoring_id = 该 UUID,一包内含关联配置)→ ② 建监控任务 → ②失败补偿删 ①(接口幂等,也可直接重试覆盖)。 - 删除:反过来,先删任务,再删规则。
- 读规则:
paging接口传entity.monitoringId精确查本任务规则。 - 关联列表语义:
null= 本次不涉及(不动既有配置),[]= 清空。两者必须区分。 - 流程清空:调既有流程删除接口(id 从规则详情的
alarmingFlowCorrelationDTO.id取),后端不新增接口。 - 孤儿规则:任务删了规则没删的残留无害,但需定期清理——筛 monitoring_id 在 security_monitoring 中已不存在的记录。
小结
- 共享兜底让改造可灰度:存量共享规则不动,任务规则增量接入,旧链路行为逐字节保持。
- 「禁用」和「不存在」必须在数据层可区分,所以查询不能预过滤状态——这决定了
findByMonitoringId不带 configStatus 条件。 - 语义升级先对齐旧运行时:「只认 ‘0’」不是洁癖,是旧查询本来就只命中 ‘0’。
- 老 SQL 加条件先跑 A/B:OR 优先级坑读代码容易漏,真实数据一跑就现形。
- 写语句和 resultMap 要一起查:threshold_status 这种「查得到、写不进」的整列缺失,单看 resultMap 永远发现不了。
改动横跨 warning / security / service-model 三模块加一条 Flyway 迁移,单测 49 个全绿,真实 SQL 与接口级 E2E 双重验证。服务器部署时需跑 Flyway CLI 应用 V20260806.12.42——迁移文件进了仓库不等于跑过,这个教训上一篇已经交过学费了。