262 lines
7.7 KiB
JavaScript
262 lines
7.7 KiB
JavaScript
|
|
"use strict";
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 聚合规则存储 + 聚合升级引擎
|
|||
|
|
*
|
|||
|
|
* 规则结构:
|
|||
|
|
* {
|
|||
|
|
* id: "agg_core_switch_offline",
|
|||
|
|
* name: "核心交换机批量离线升级",
|
|||
|
|
* group_by: ["catalog_type", "tier"], // 维度(D6:catalog_type + tier)
|
|||
|
|
* window_minutes: 5,
|
|||
|
|
* threshold_count: 3,
|
|||
|
|
* alert_type_filter: ["connectivity"],
|
|||
|
|
* level_filter: ["critical"],
|
|||
|
|
* target_level: "critical",
|
|||
|
|
* enabled: true,
|
|||
|
|
* }
|
|||
|
|
*
|
|||
|
|
* 触发方式:实时(D2)—— 每来一条新告警就检查窗口内计数
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const { STORAGE_KEY_AGGREGATION_RULES } = require("./storageKeys.cjs");
|
|||
|
|
|
|||
|
|
let _rules = null;
|
|||
|
|
|
|||
|
|
function _ensureLoaded(readFn) {
|
|||
|
|
if (_rules === null) {
|
|||
|
|
const raw = readFn(STORAGE_KEY_AGGREGATION_RULES);
|
|||
|
|
_rules = raw ? _safeParse(raw, []) : _getDefaultRules();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _safeParse(text, def) {
|
|||
|
|
if (text === null || text === undefined) return def;
|
|||
|
|
if (typeof text === "object") return text;
|
|||
|
|
try { return JSON.parse(text); }
|
|||
|
|
catch { return def; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _save(writeFn) {
|
|||
|
|
writeFn(STORAGE_KEY_AGGREGATION_RULES, JSON.stringify(_rules));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _getDefaultRules() {
|
|||
|
|
const now = Date.now();
|
|||
|
|
return [
|
|||
|
|
{
|
|||
|
|
id: "agg_core_switch_offline",
|
|||
|
|
name: "核心设备批量离线升级",
|
|||
|
|
group_by: ["catalog_type", "tier"],
|
|||
|
|
window_minutes: 5,
|
|||
|
|
threshold_count: 3,
|
|||
|
|
alert_type_filter: ["connectivity"],
|
|||
|
|
level_filter: ["critical"],
|
|||
|
|
target_level: "critical",
|
|||
|
|
enabled: true,
|
|||
|
|
builtin: true,
|
|||
|
|
created_at: now,
|
|||
|
|
updated_at: now,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "agg_core_performance_spike",
|
|||
|
|
name: "核心设备性能批量异常升级",
|
|||
|
|
group_by: ["catalog_type", "tier"],
|
|||
|
|
window_minutes: 10,
|
|||
|
|
threshold_count: 5,
|
|||
|
|
alert_type_filter: ["performance_high"],
|
|||
|
|
level_filter: ["warning", "critical"],
|
|||
|
|
target_level: "critical",
|
|||
|
|
enabled: true,
|
|||
|
|
builtin: true,
|
|||
|
|
created_at: now,
|
|||
|
|
updated_at: now,
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 规则 CRUD ----------
|
|||
|
|
|
|||
|
|
function listRules(readFn) {
|
|||
|
|
_ensureLoaded(readFn);
|
|||
|
|
return [..._rules];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getRule(readFn, ruleId) {
|
|||
|
|
_ensureLoaded(readFn);
|
|||
|
|
return _rules.find((r) => r.id === ruleId) || null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function createRule(readFn, writeFn, data) {
|
|||
|
|
_ensureLoaded(readFn);
|
|||
|
|
const rule = {
|
|||
|
|
id: data.id || `agg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|||
|
|
name: data.name,
|
|||
|
|
group_by: data.group_by || ["catalog_type", "tier"],
|
|||
|
|
window_minutes: data.window_minutes || 5,
|
|||
|
|
threshold_count: data.threshold_count || 3,
|
|||
|
|
alert_type_filter: data.alert_type_filter || [],
|
|||
|
|
level_filter: data.level_filter || [],
|
|||
|
|
target_level: data.target_level || "critical",
|
|||
|
|
enabled: data.enabled !== false,
|
|||
|
|
builtin: false,
|
|||
|
|
created_at: Date.now(),
|
|||
|
|
updated_at: Date.now(),
|
|||
|
|
};
|
|||
|
|
_rules.push(rule);
|
|||
|
|
_save(writeFn);
|
|||
|
|
return { ...rule };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateRule(readFn, writeFn, ruleId, updates) {
|
|||
|
|
_ensureLoaded(readFn);
|
|||
|
|
const idx = _rules.findIndex((r) => r.id === ruleId);
|
|||
|
|
if (idx === -1) return null;
|
|||
|
|
if (_rules[idx].builtin) return null;
|
|||
|
|
_rules[idx] = { ..._rules[idx], ...updates, updated_at: Date.now() };
|
|||
|
|
_save(writeFn);
|
|||
|
|
return { ..._rules[idx] };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function deleteRule(readFn, writeFn, ruleId) {
|
|||
|
|
_ensureLoaded(readFn);
|
|||
|
|
const idx = _rules.findIndex((r) => r.id === ruleId);
|
|||
|
|
if (idx === -1) return false;
|
|||
|
|
if (_rules[idx].builtin) return false;
|
|||
|
|
_rules.splice(idx, 1);
|
|||
|
|
_save(writeFn);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 实时聚合检查(D2:每来一条新告警就调用)----------
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查窗口内活跃告警是否命中聚合规则
|
|||
|
|
* @param {object} readFn - 存储读取函数
|
|||
|
|
* @param {Array} activeAlerts - alertRecordStore 返回的活跃告警列表
|
|||
|
|
* @param {object} deviceProfiles - { [device_id]: { catalog_type, tier, ... } }
|
|||
|
|
* @returns {Array} 命中的聚合规则 [{ rule, groupKey, matchingAlerts }]
|
|||
|
|
*/
|
|||
|
|
function checkAggregation(readFn, activeAlerts, deviceProfiles) {
|
|||
|
|
_ensureLoaded(readFn);
|
|||
|
|
const hits = [];
|
|||
|
|
const now = Date.now();
|
|||
|
|
|
|||
|
|
for (const rule of _rules) {
|
|||
|
|
if (!rule.enabled) continue;
|
|||
|
|
const windowMs = rule.window_minutes * 60 * 1000;
|
|||
|
|
const windowStart = now - windowMs;
|
|||
|
|
|
|||
|
|
// 过滤时间窗口内的活跃告警
|
|||
|
|
const inWindow = activeAlerts.filter((a) => a.triggered_at >= windowStart);
|
|||
|
|
|
|||
|
|
// 按 group_by 维度分组
|
|||
|
|
const groups = new Map();
|
|||
|
|
for (const alert of inWindow) {
|
|||
|
|
const profile = deviceProfiles?.[alert.device_id] || {};
|
|||
|
|
let keyParts = [];
|
|||
|
|
for (const dim of rule.group_by) {
|
|||
|
|
if (dim === "catalog_type") keyParts.push(profile.catalog_type || alert.alert_catalog || "unknown");
|
|||
|
|
else if (dim === "tier") keyParts.push(profile.tier || "unknown");
|
|||
|
|
else keyParts.push("*");
|
|||
|
|
}
|
|||
|
|
const groupKey = keyParts.join("::");
|
|||
|
|
|
|||
|
|
// alert_type / level 过滤
|
|||
|
|
if (rule.alert_type_filter?.length > 0) {
|
|||
|
|
const at = alert.alert_type || "";
|
|||
|
|
if (!rule.alert_type_filter.includes(at)) continue;
|
|||
|
|
}
|
|||
|
|
if (rule.level_filter?.length > 0 && !rule.level_filter.includes(alert.level)) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|||
|
|
groups.get(groupKey).push(alert);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查阈值
|
|||
|
|
for (const [groupKey, matchingAlerts] of groups) {
|
|||
|
|
if (matchingAlerts.length >= rule.threshold_count) {
|
|||
|
|
hits.push({ rule, groupKey, matchingAlerts });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return hits;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 升级告警(产出新 alertRecord)
|
|||
|
|
* @param {object} readFn - 存储读取函数
|
|||
|
|
* @param {object} writeFn - 存储写入函数
|
|||
|
|
* @param {string} reason - 升级原因
|
|||
|
|
* @param {object} data - { source_alert_ids, target_level, message }
|
|||
|
|
* @returns {object} 新告警记录
|
|||
|
|
*/
|
|||
|
|
function escalateAlert(readFn, writeFn, reason, data) {
|
|||
|
|
const { alertRecordStore } = require("./alertRecordStore.cjs");
|
|||
|
|
const alert = alertRecordStore._createAlert
|
|||
|
|
? _createEscalationAlert(reason, data)
|
|||
|
|
: _createEscalationAlert(reason, data);
|
|||
|
|
|
|||
|
|
// 直接写到 alertRecordStore(复用内部函数)
|
|||
|
|
const alerts = _loadAlerts(readFn);
|
|||
|
|
alerts.push(alert);
|
|||
|
|
_saveAlerts(writeFn, alerts);
|
|||
|
|
return alert;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// alertRecordStore 内部结构复用(直接操作存储避免循环依赖)
|
|||
|
|
function _loadAlerts(readFn) {
|
|||
|
|
const raw = readFn("netcatty_inspection_alert_records_v1");
|
|||
|
|
return raw ? (typeof raw === "string" ? JSON.parse(raw) : raw) : [];
|
|||
|
|
}
|
|||
|
|
function _saveAlerts(writeFn, alerts) {
|
|||
|
|
writeFn("netcatty_inspection_alert_records_v1", JSON.stringify(alerts));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _createEscalationAlert(reason, data) {
|
|||
|
|
const now = Date.now();
|
|||
|
|
const deviceNames = (data.source_alert_ids || []).join(", ");
|
|||
|
|
return {
|
|||
|
|
id: `alert_esc_${now}_${Math.random().toString(36).slice(2, 8)}`,
|
|||
|
|
rule_id: `rule_escalation_${reason}`,
|
|||
|
|
rule_name: reason === "aggregation" ? "聚合升级" : "超时升级",
|
|||
|
|
device_id: "group_escalation",
|
|||
|
|
device_name: `[升级告警] ${deviceNames}`,
|
|||
|
|
metric: "escalation",
|
|||
|
|
metric_value: data.source_alert_ids?.length || 0,
|
|||
|
|
threshold: data.target_level || "critical",
|
|||
|
|
level: data.target_level || "critical",
|
|||
|
|
message: data.message || `${reason}: ${data.source_alert_ids?.length || 0} 台设备告警触发升级`,
|
|||
|
|
triggered_at: now,
|
|||
|
|
status: "active",
|
|||
|
|
acknowledged_by: null,
|
|||
|
|
acknowledged_at: null,
|
|||
|
|
resolved_at: null,
|
|||
|
|
resolved_reason: null,
|
|||
|
|
note: null,
|
|||
|
|
// v1.3 扩展字段
|
|||
|
|
escalation: {
|
|||
|
|
trigger: reason, // aggregation | escalation_fallback_timeout
|
|||
|
|
source_alert_ids: data.source_alert_ids || [],
|
|||
|
|
target_level: data.target_level || "critical",
|
|||
|
|
escalated_at: now,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _resetForTest() { _rules = null; }
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
listRules,
|
|||
|
|
getRule,
|
|||
|
|
createRule,
|
|||
|
|
updateRule,
|
|||
|
|
deleteRule,
|
|||
|
|
checkAggregation,
|
|||
|
|
escalateAlert,
|
|||
|
|
_resetForTest,
|
|||
|
|
};
|