Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
544 lines
14 KiB
JavaScript
544 lines
14 KiB
JavaScript
"use strict";
|
||
|
||
/**
|
||
* 告警规则数据存储
|
||
*
|
||
* 告警规则结构:
|
||
* {
|
||
* id: "rule_xxx",
|
||
* name: "CPU 使用率过高",
|
||
* metric: "cpu_usage",
|
||
* operator: ">",
|
||
* threshold: "80",
|
||
* level: "critical",
|
||
* duration: 1, // 连续超标次数(防抖动)
|
||
* scope_type: "all", // all / tier / device / catalog
|
||
* scope_value: null, // 范围值(tier_key 或 hostId 数组 或 catalog_id)
|
||
* enabled: true,
|
||
* created_at: timestamp,
|
||
* updated_at: timestamp,
|
||
* }
|
||
*/
|
||
|
||
const { STORAGE_KEY_ALERT_RULES } = require("./storageKeys.cjs");
|
||
const { OPERATORS, ALERT_LEVELS } = require("./types.cjs");
|
||
|
||
let _rules = null;
|
||
|
||
function _ensureLoaded(readFn) {
|
||
if (_rules === null) {
|
||
const raw = readFn(STORAGE_KEY_ALERT_RULES);
|
||
_rules = raw ? _safeParse(raw, []) : _getDefaultRules();
|
||
}
|
||
}
|
||
|
||
function _safeParse(text, def) {
|
||
try { return JSON.parse(text); }
|
||
catch (e) { return def; }
|
||
}
|
||
|
||
function _save(writeFn) {
|
||
writeFn(STORAGE_KEY_ALERT_RULES, JSON.stringify(_rules));
|
||
}
|
||
|
||
/**
|
||
* 默认告警规则(预置)
|
||
*/
|
||
function _getDefaultRules() {
|
||
return [
|
||
{
|
||
id: "rule_cpu_critical",
|
||
name: "CPU 使用率过高",
|
||
metric: "cpu_usage",
|
||
operator: OPERATORS.GT,
|
||
threshold: "80",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 1,
|
||
scope_type: "all",
|
||
scope_value: null,
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_cpu_warning",
|
||
name: "CPU 使用率异常",
|
||
metric: "cpu_usage",
|
||
operator: OPERATORS.GT,
|
||
threshold: "70",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 2,
|
||
scope_type: "all",
|
||
scope_value: null,
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_memory_critical",
|
||
name: "内存使用率过高",
|
||
metric: "memory_usage",
|
||
operator: OPERATORS.GT,
|
||
threshold: "85",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 1,
|
||
scope_type: "all",
|
||
scope_value: null,
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_memory_warning",
|
||
name: "内存使用率异常",
|
||
metric: "memory_usage",
|
||
operator: OPERATORS.GT,
|
||
threshold: "75",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 2,
|
||
scope_type: "all",
|
||
scope_value: null,
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_offline",
|
||
name: "设备离线",
|
||
metric: "online",
|
||
operator: OPERATORS.EQ,
|
||
threshold: "false",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 1,
|
||
scope_type: "all",
|
||
scope_value: null,
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
// ---------- 磁盘使用率 ----------
|
||
{
|
||
id: "rule_disk_critical",
|
||
name: "磁盘使用率过高",
|
||
metric: "disk_usage",
|
||
operator: OPERATORS.GT,
|
||
threshold: "90",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 1,
|
||
scope_type: "all",
|
||
scope_value: null,
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_disk_warning",
|
||
name: "磁盘使用率异常",
|
||
metric: "disk_usage",
|
||
operator: OPERATORS.GT,
|
||
threshold: "80",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 2,
|
||
scope_type: "all",
|
||
scope_value: null,
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
// ---------- 网络设备通用(交换机/防火墙) ----------
|
||
{
|
||
id: "rule_netdev_temp_critical",
|
||
name: "设备温度过高",
|
||
metric: "temperature",
|
||
operator: OPERATORS.GT,
|
||
threshold: "60",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 2,
|
||
scope_type: "catalog",
|
||
scope_value: "huawei_h3c_switch",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_netdev_power_critical",
|
||
name: "电源模块故障",
|
||
metric: "power_failure_count",
|
||
operator: OPERATORS.GT,
|
||
threshold: "0",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 1,
|
||
scope_type: "catalog",
|
||
scope_value: "huawei_h3c_switch",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_netdev_fan_warning",
|
||
name: "风扇模块故障",
|
||
metric: "fan_failure_count",
|
||
operator: OPERATORS.GT,
|
||
threshold: "0",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 1,
|
||
scope_type: "catalog",
|
||
scope_value: "huawei_h3c_switch",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_netdev_portdown_warning",
|
||
name: "端口 Down 数过多",
|
||
metric: "port_down_count",
|
||
operator: OPERATORS.GT,
|
||
threshold: "2",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 2,
|
||
scope_type: "catalog",
|
||
scope_value: "huawei_h3c_switch",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_netdev_cpu_warning",
|
||
name: "交换机 CPU 使用率过高",
|
||
metric: "cpu_usage",
|
||
operator: OPERATORS.GT,
|
||
threshold: "75",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 2,
|
||
scope_type: "catalog",
|
||
scope_value: "huawei_h3c_switch",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_netdev_memory_warning",
|
||
name: "交换机内存使用率过高",
|
||
metric: "memory_usage",
|
||
operator: OPERATORS.GT,
|
||
threshold: "80",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 2,
|
||
scope_type: "catalog",
|
||
scope_value: "huawei_h3c_switch",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
// ---------- Cisco 交换机 ----------
|
||
{
|
||
id: "rule_cisco_temp_critical",
|
||
name: "Cisco 交换机温度过高",
|
||
metric: "temperature",
|
||
operator: OPERATORS.GT,
|
||
threshold: "60",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 2,
|
||
scope_type: "catalog",
|
||
scope_value: "cisco_switch",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_cisco_power_critical",
|
||
name: "Cisco 电源模块故障",
|
||
metric: "power_failure_count",
|
||
operator: OPERATORS.GT,
|
||
threshold: "0",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 1,
|
||
scope_type: "catalog",
|
||
scope_value: "cisco_switch",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_cisco_fan_warning",
|
||
name: "Cisco 风扇模块故障",
|
||
metric: "fan_failure_count",
|
||
operator: OPERATORS.GT,
|
||
threshold: "0",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 1,
|
||
scope_type: "catalog",
|
||
scope_value: "cisco_switch",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
// ---------- 华为防火墙 ----------
|
||
{
|
||
id: "rule_hwfw_temp_critical",
|
||
name: "华为防火墙温度过高",
|
||
metric: "temperature",
|
||
operator: OPERATORS.GT,
|
||
threshold: "60",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 2,
|
||
scope_type: "catalog",
|
||
scope_value: "huawei_firewall",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_hwfw_power_critical",
|
||
name: "华为防火墙电源故障",
|
||
metric: "power_failure_count",
|
||
operator: OPERATORS.GT,
|
||
threshold: "0",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 1,
|
||
scope_type: "catalog",
|
||
scope_value: "huawei_firewall",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_hwfw_fan_warning",
|
||
name: "华为防火墙风扇故障",
|
||
metric: "fan_failure_count",
|
||
operator: OPERATORS.GT,
|
||
threshold: "0",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 1,
|
||
scope_type: "catalog",
|
||
scope_value: "huawei_firewall",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
// ---------- 飞塔防火墙 ----------
|
||
{
|
||
id: "rule_fortinet_temp_critical",
|
||
name: "飞塔防火墙温度过高",
|
||
metric: "temperature",
|
||
operator: OPERATORS.GT,
|
||
threshold: "60",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 2,
|
||
scope_type: "catalog",
|
||
scope_value: "fortinet_firewall",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_fortinet_power_critical",
|
||
name: "飞塔防火墙电源故障",
|
||
metric: "power_failure_count",
|
||
operator: OPERATORS.GT,
|
||
threshold: "0",
|
||
level: ALERT_LEVELS.CRITICAL,
|
||
duration: 1,
|
||
scope_type: "catalog",
|
||
scope_value: "fortinet_firewall",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.now(),
|
||
},
|
||
{
|
||
id: "rule_fortinet_fan_warning",
|
||
name: "飞塔防火墙风扇故障",
|
||
metric: "fan_failure_count",
|
||
operator: OPERATORS.GT,
|
||
threshold: "0",
|
||
level: ALERT_LEVELS.WARNING,
|
||
duration: 1,
|
||
scope_type: "catalog",
|
||
scope_value: "fortinet_firewall",
|
||
enabled: true,
|
||
preset: true,
|
||
created_at: Date.now(),
|
||
updated_at: Date.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: `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||
name: data.name,
|
||
metric: data.metric,
|
||
operator: data.operator,
|
||
threshold: String(data.threshold),
|
||
level: data.level,
|
||
duration: data.duration || 1,
|
||
scope_type: data.scope_type || "all",
|
||
scope_value: data.scope_value || null,
|
||
enabled: data.enabled !== false,
|
||
preset: 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;
|
||
|
||
_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;
|
||
|
||
_rules.splice(idx, 1);
|
||
_save(writeFn);
|
||
return true;
|
||
}
|
||
|
||
function toggleRule(readFn, writeFn, ruleId, enabled) {
|
||
return updateRule(readFn, writeFn, ruleId, { enabled });
|
||
}
|
||
|
||
// ---------- 告警判断 ----------
|
||
|
||
/**
|
||
* 判断指标值是否触发规则
|
||
* @param {number|string} value - 指标值
|
||
* @param {string} operator - 比较符
|
||
* @param {string} threshold - 阈值
|
||
* @returns {boolean}
|
||
*/
|
||
function evaluateRule(value, operator, threshold) {
|
||
if (value === null || value === undefined) return false;
|
||
|
||
// disk_usage 是数组,检查任意挂载点是否超标
|
||
if (Array.isArray(value)) {
|
||
const numThreshold = parseFloat(threshold);
|
||
if (isNaN(numThreshold)) return false;
|
||
// 数组中每个元素是 { filesystem, usage_percent, mount }
|
||
return value.some((d) => {
|
||
const usage = typeof d === "object" ? d.usage_percent : parseFloat(d);
|
||
return !isNaN(usage) && usage > numThreshold;
|
||
});
|
||
}
|
||
|
||
// 数字比较
|
||
const numValue = parseFloat(value);
|
||
const numThreshold = parseFloat(threshold);
|
||
|
||
if (!isNaN(numValue) && !isNaN(numThreshold)) {
|
||
switch (operator) {
|
||
case OPERATORS.GT: return numValue > numThreshold;
|
||
case OPERATORS.LT: return numValue < numThreshold;
|
||
case OPERATORS.GTE: return numValue >= numThreshold;
|
||
case OPERATORS.LTE: return numValue <= numThreshold;
|
||
case OPERATORS.EQ: return numValue === numThreshold;
|
||
case OPERATORS.NE: return numValue !== numThreshold;
|
||
}
|
||
}
|
||
|
||
// 字符串比较
|
||
const strValue = String(value);
|
||
const strThreshold = String(threshold);
|
||
switch (operator) {
|
||
case OPERATORS.EQ: return strValue === strThreshold;
|
||
case OPERATORS.NE: return strValue !== strThreshold;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取设备适用的所有规则
|
||
* @param {object} deviceExt - 设备扩展信息
|
||
* @returns {Array} 适用的规则列表
|
||
*/
|
||
function getRulesForDevice(readFn, deviceExt) {
|
||
_ensureLoaded(readFn);
|
||
|
||
return _rules.filter((rule) => {
|
||
if (!rule.enabled) return false;
|
||
|
||
switch (rule.scope_type) {
|
||
case "all":
|
||
return true;
|
||
case "tier":
|
||
return deviceExt.tier && deviceExt.tier === rule.scope_value;
|
||
case "device":
|
||
return Array.isArray(rule.scope_value) && rule.scope_value.includes(deviceExt.host_id);
|
||
case "catalog":
|
||
return deviceExt.catalog_id === rule.scope_value;
|
||
default:
|
||
return false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// ---------- 重置(测试用) ----------
|
||
function _resetForTest() {
|
||
_rules = null;
|
||
}
|
||
|
||
module.exports = {
|
||
listRules,
|
||
getRule,
|
||
createRule,
|
||
updateRule,
|
||
deleteRule,
|
||
toggleRule,
|
||
evaluateRule,
|
||
getRulesForDevice,
|
||
_resetForTest,
|
||
};
|