Files
SenMeshNet/electron/bridges/inspection/alertStateMonitor.cjs

152 lines
5.5 KiB
JavaScript
Raw Permalink Normal View History

"use strict";
/**
* AlertStateMonitor(D23:每 5min 定时扫活跃告警)
*
* 设计:
* - 现有 alertAggregationEngine 是"事件驱动"(来一条新告警检查一次),
* 不会主动扫存量告警的超时情况
* - AlertStateMonitor 每 5min 跑一次,检查:
* ① 告警挂了多久 → 超 escalation_fallback.auto_escalate_minutes → 自动升级
* ② remediation.status === "running" 超时 → 暂停处置 + 通知值班员
* ③ remediation.status === "completed" → 检查 auto_close_after_remediation_recovery
*/
const _CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 分钟
const _REMEDIATION_TIMEOUT_MS = 30 * 60 * 1000; // 30 分钟硬阈值
let _timer = null;
let _isRunning = false;
/**
* 启动定时监控(应在巡检模块初始化时调用一次)
* @param {object} deps - 依赖注入
* @param {function} deps.readFn - 存储读取函数
* @param {function} deps.writeFn - 存储写入函数
* @param {object} deps.alertRecordStore - alertRecordStore 模块
* @param {object} deps.aggregationEngine - aggregationEngine 模块
* @param {object} deps.remediationEngine - remediationEngine 模块
* @param {function} [deps.onNotification] - 通知回调
*/
function start(deps) {
if (_timer) return; // 已启动
const { readFn, writeFn, alertRecordStore, aggregationEngine, remediationEngine, onNotification } = deps;
async function run() {
if (_isRunning) return; // 避免重叠
_isRunning = true;
try {
const activeAlerts = alertRecordStore.listAlerts(readFn, { limit: 500 });
const eligibleAlerts = activeAlerts.filter(
(a) => a.status === "active" || a.status === "acknowledged"
);
for (const alert of eligibleAlerts) {
await _checkOne(alert, { readFn, writeFn, aggregationEngine, remediationEngine, alertRecordStore, onNotification });
}
} catch (e) {
console.error("[AlertStateMonitor] run error:", e.message);
} finally {
_isRunning = false;
}
}
_timer = setInterval(run, _CHECK_INTERVAL_MS);
console.info("[AlertStateMonitor] 已启动,每 5 分钟检查一次活跃告警");
// 立即跑一次
run();
}
function stop() {
if (_timer) {
clearInterval(_timer);
_timer = null;
console.info("[AlertStateMonitor] 已停止");
}
}
function isRunning() { return !!_timer; }
async function _checkOne(alert, deps) {
const { readFn, writeFn, aggregationEngine, remediationEngine, alertRecordStore, onNotification } = deps;
const snapshot = alert.matched_plan_snapshot;
if (!snapshot) return; // 没匹配预案的不处理
const now = Date.now();
const elapsedMin = (now - alert.triggered_at) / 60000;
// ── 检查 1:escalation_fallback.auto_escalate_minutes ──
const fallback = snapshot.escalation_fallback;
if (fallback?.auto_escalate_minutes && elapsedMin >= fallback.auto_escalate_minutes) {
// 避免重复升级:同一条告警只升级一次
const alreadyEscalated = (alert.audit_log || []).some(
(a) => a.action === "escalation_fallback_timeout" || a.action === "aggregation_escalation"
);
if (!alreadyEscalated) {
console.warn(`[AlertStateMonitor] 告警 ${alert.id} 挂了 ${elapsedMin.toFixed(0)}min,触发自动升级`);
aggregationEngine.escalateAlert(readFn, writeFn, "escalation_fallback_timeout", {
source_alert_ids: [alert.id],
target_level: fallback.escalation_target_level || alert.level,
message: `告警挂 ${elapsedMin.toFixed(0)}min 未处置,自动升级`,
});
// 追加审计
_appendAudit(alertRecordStore, readFn, writeFn, alert.id, {
ts: now, actor: "alertStateMonitor", action: "escalation_fallback_timeout",
detail: `elapsed=${elapsedMin.toFixed(0)}min`,
});
onNotification?.({ type: "escalation", alertId: alert.id });
}
}
// ── 检查 2:remediation 运行超时 ──
const remediation = alert.remediation;
if (remediation?.status === "running") {
const runningFor = now - (remediation.started_at || 0);
if (runningFor > _REMEDIATION_TIMEOUT_MS) {
console.warn(`[AlertStateMonitor] 告警 ${alert.id} 处置运行 ${(runningFor / 60000).toFixed(0)}min,超时暂停`);
remediationEngine.pause(alert.id);
_appendAudit(alertRecordStore, readFn, writeFn, alert.id, {
ts: now, actor: "alertStateMonitor", action: "remediation_timeout",
detail: `running=${(runningFor / 60000).toFixed(0)}min,自动暂停`,
});
onNotification?.({ type: "remediation_timeout", alertId: alert.id });
}
}
}
function _appendAudit(alertRecordStore, readFn, writeFn, alertId, auditEntry) {
try {
// 直接读写底层存储(避免 alertRecordStore 暴露内部方法)
const raw = readFn("netcatty_inspection_alert_records_v1");
const alerts = raw ? (typeof raw === "string" ? JSON.parse(raw) : raw) : [];
const idx = alerts.findIndex((a) => a.id === alertId);
if (idx >= 0) {
alerts[idx].audit_log = [...(alerts[idx].audit_log || []), auditEntry];
writeFn("netcatty_inspection_alert_records_v1", JSON.stringify(alerts));
}
} catch (e) {
console.warn("[AlertStateMonitor] 追加审计失败:", e.message);
}
}
function getStatus() {
return {
running: isRunning(),
interval_minutes: _CHECK_INTERVAL_MS / 60000,
remediation_timeout_minutes: _REMEDIATION_TIMEOUT_MS / 60000,
};
}
module.exports = {
start,
stop,
isRunning,
getStatus,
// 测试用
_CHECK_INTERVAL_MS,
_REMEDIATION_TIMEOUT_MS,
};