feat: 监控告警体系统一重构 + 自动化任务调度引擎 + 监控大屏拆分
告警中心 - 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog) - 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则 - 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp) 监控采集 - SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect) - 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed) - 站点探测 pkg(siteprobe) + 站点视图(site_view) 自动化任务 - cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task) 审计与凭证 - 审计设置(audit_setting) + 历史清理(auditcleaner) 前端 - 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化 - 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染 - 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
092d0e4b9e
commit
471353e3d7
190 files changed
+16640
-3484
No files matched your search
@@ -0,0 +1,251 @@
|
||||
// 中间件监控可视化辅助 // Logan.Li
|
||||
|
||||
export interface MwConfigPublic {
|
||||
username?: string;
|
||||
database?: string;
|
||||
redis_mode?: 'standalone' | 'cluster';
|
||||
}
|
||||
|
||||
export interface MwTarget {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
description: string;
|
||||
sub_type?: string;
|
||||
config_public?: MwConfigPublic;
|
||||
latest_metrics: Record<string, number>;
|
||||
last_seen_at?: string;
|
||||
}
|
||||
|
||||
export interface MwHistoryPoint {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
metric_name?: string;
|
||||
}
|
||||
|
||||
export type MwTimeRange = '1h' | '6h' | '24h' | '7d';
|
||||
|
||||
export const MW_TIME_RANGE_HOURS: Record<MwTimeRange, number> = {
|
||||
'1h': 1,
|
||||
'6h': 6,
|
||||
'24h': 24,
|
||||
'7d': 168,
|
||||
};
|
||||
|
||||
export interface MwChartDef {
|
||||
key: string;
|
||||
label: string;
|
||||
color: string;
|
||||
unit?: string;
|
||||
percent?: boolean;
|
||||
formatter?: (v: number) => string;
|
||||
/** 累计 counter 转周期增量 // Logan.Li */
|
||||
counterDelta?: boolean;
|
||||
}
|
||||
|
||||
export type HealthLevel = 'ok' | 'warn' | 'crit';
|
||||
|
||||
export interface MwHealthIssue {
|
||||
level: HealthLevel;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const REDIS_PRIMARY_KEYS = ['redis_qps', 'redis_used_memory', 'redis_hit_rate', 'redis_connected_clients'];
|
||||
export const MYSQL_PRIMARY_KEYS = ['mysql_qps', 'mysql_connections', 'mysql_buffer_hit_rate', 'mysql_slow_queries'];
|
||||
|
||||
export const REDIS_CHARTS: MwChartDef[] = [
|
||||
{ key: 'redis_qps', label: 'QPS', color: 'var(--dg-blue)', unit: 'ops/s' },
|
||||
{ key: 'redis_used_memory', label: '内存占用', color: 'var(--dg-red)', formatter: fmtMemory },
|
||||
{ key: 'redis_hit_rate', label: '命中率', color: 'var(--dg-green)', percent: true },
|
||||
{ key: 'redis_connected_clients', label: '连接数', color: 'var(--dg-cyan)' },
|
||||
{ key: 'redis_db_size', label: 'Key 总数', color: 'var(--dg-purple)' },
|
||||
{ key: 'redis_blocked_clients', label: '阻塞客户端', color: 'var(--dg-orange)' },
|
||||
];
|
||||
|
||||
export const MYSQL_CHARTS: MwChartDef[] = [
|
||||
{ key: 'mysql_qps', label: 'QPS', color: 'var(--dg-blue)', unit: 'q/s' },
|
||||
{ key: 'mysql_connections', label: '连接数', color: 'var(--dg-cyan)' },
|
||||
{ key: 'mysql_threads_running', label: '活跃线程', color: 'var(--dg-purple)' },
|
||||
{ key: 'mysql_buffer_hit_rate', label: '缓冲池命中率', color: 'var(--dg-green)', percent: true },
|
||||
{ key: 'mysql_slow_queries', label: '慢查询速率', color: 'var(--dg-orange)', unit: '次/周期', counterDelta: true },
|
||||
{ key: 'mysql_bytes_sent', label: '发送流量', color: 'var(--dg-amber)', formatter: fmtMemory },
|
||||
];
|
||||
|
||||
export function fmtMemory(bytes: number): string {
|
||||
if (bytes == null || bytes === 0) return '--';
|
||||
if (bytes > 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
|
||||
if (bytes > 1e6) return `${(bytes / 1e6).toFixed(1)} MB`;
|
||||
if (bytes > 1e3) return `${(bytes / 1e3).toFixed(1)} KB`;
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
export function fmtHitRate(v: number): string {
|
||||
if (v == null || Number.isNaN(v)) return '--';
|
||||
return `${(v * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
export function fmtVal(v: number, precision = 0): string {
|
||||
if (v == null || Number.isNaN(v)) return '--';
|
||||
return v.toFixed(precision);
|
||||
}
|
||||
|
||||
export function fmtChartTime(ts: number): string {
|
||||
const d = new Date(ts * 1000);
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
const hh = String(d.getHours()).padStart(2, '0');
|
||||
const mi = String(d.getMinutes()).padStart(2, '0');
|
||||
return `${mm}-${dd} ${hh}:${mi}`;
|
||||
}
|
||||
|
||||
export function fmtMetricDisplay(def: MwChartDef, value: number): string {
|
||||
if (def.formatter) return def.formatter(value);
|
||||
if (def.percent) return fmtHitRate(value);
|
||||
return fmtVal(value, def.key.includes('qps') ? 1 : 0);
|
||||
}
|
||||
|
||||
export function statusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'online': return 'var(--dg-green)';
|
||||
case 'warning': return 'var(--dg-yellow)';
|
||||
case 'error':
|
||||
case 'offline': return 'var(--dg-red)';
|
||||
default: return 'var(--dg-text-muted)';
|
||||
}
|
||||
}
|
||||
|
||||
export function statusTextClass(status: string): string {
|
||||
switch (status) {
|
||||
case 'online': return 'dg-text-green';
|
||||
case 'warning': return 'dg-text-yellow';
|
||||
case 'error':
|
||||
case 'offline': return 'dg-text-red';
|
||||
default: return 'dg-text-muted';
|
||||
}
|
||||
}
|
||||
|
||||
export function statusBg(status: string): string {
|
||||
switch (status) {
|
||||
case 'online': return 'bg-emerald-400';
|
||||
case 'warning': return 'bg-amber-400';
|
||||
case 'error':
|
||||
case 'offline': return 'bg-red-400';
|
||||
default: return 'bg-[var(--dg-bg-active)]';
|
||||
}
|
||||
}
|
||||
|
||||
export function redisModeLabel(mode?: string): string {
|
||||
return mode === 'cluster' ? '集群' : '单机';
|
||||
}
|
||||
|
||||
export function redisMemoryPct(metrics: Record<string, number>): number | null {
|
||||
const used = metrics.redis_used_memory;
|
||||
const max = metrics.redis_maxmemory;
|
||||
if (!used || !max || max <= 0) return null;
|
||||
return Math.min(100, (used / max) * 100);
|
||||
}
|
||||
|
||||
export function mysqlConnPct(metrics: Record<string, number>): number | null {
|
||||
const conn = metrics.mysql_connections;
|
||||
const max = metrics.mysql_max_connections;
|
||||
if (!conn || !max || max <= 0) return null;
|
||||
return Math.min(100, (conn / max) * 100);
|
||||
}
|
||||
|
||||
/** 累计 counter 转周期增量(如慢查询)// Logan.Li */
|
||||
export function counterToDelta(
|
||||
points: Array<{ timestamp: number; value: number }>,
|
||||
): Array<{ timestamp: number; value: number }> {
|
||||
if (points.length < 2) return [];
|
||||
const out: Array<{ timestamp: number; value: number }> = [];
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = points[i - 1];
|
||||
const cur = points[i];
|
||||
const delta = cur.value - prev.value;
|
||||
if (delta >= 0) out.push({ timestamp: cur.timestamp, value: delta });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 按指标名分组历史时序 // Logan.Li */
|
||||
export function groupHistoryByMetric(
|
||||
rows: Array<{ metric_name: string; timestamp: number; value: number }>,
|
||||
): Record<string, Array<{ timestamp: number; value: number }>> {
|
||||
const out: Record<string, Array<{ timestamp: number; value: number }>> = {};
|
||||
for (const row of rows) {
|
||||
if (!out[row.metric_name]) out[row.metric_name] = [];
|
||||
out[row.metric_name].push({ timestamp: row.timestamp, value: row.value });
|
||||
}
|
||||
for (const key of Object.keys(out)) {
|
||||
out[key].sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function gaugeTone(pct: number, warn = 70, crit = 85): HealthLevel {
|
||||
if (pct >= crit) return 'crit';
|
||||
if (pct >= warn) return 'warn';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
export function gaugeToneLow(pct: number, warn = 90, crit = 80): HealthLevel {
|
||||
if (pct < crit) return 'crit';
|
||||
if (pct < warn) return 'warn';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
export function healthLevelColor(level: HealthLevel): string {
|
||||
if (level === 'crit') return 'var(--dg-red)';
|
||||
if (level === 'warn') return 'var(--dg-yellow)';
|
||||
return 'var(--dg-green)';
|
||||
}
|
||||
|
||||
/** 评估实例健康告警 // Logan.Li */
|
||||
export function evalMwHealth(target: MwTarget): MwHealthIssue[] {
|
||||
const m = target.latest_metrics || {};
|
||||
const issues: MwHealthIssue[] = [];
|
||||
if (target.status !== 'online') {
|
||||
issues.push({ level: 'crit', message: '实例离线' });
|
||||
return issues;
|
||||
}
|
||||
if (target.description === 'redis') {
|
||||
if (m.redis_hit_rate > 0 && m.redis_hit_rate < 0.8) issues.push({ level: 'crit', message: '命中率 < 80%' });
|
||||
else if (m.redis_hit_rate > 0 && m.redis_hit_rate < 0.9) issues.push({ level: 'warn', message: '命中率 < 90%' });
|
||||
const memPct = redisMemoryPct(m);
|
||||
if (memPct != null && memPct > 90) issues.push({ level: 'crit', message: '内存 > 90%' });
|
||||
else if (memPct != null && memPct > 80) issues.push({ level: 'warn', message: '内存 > 80%' });
|
||||
if (m.redis_blocked_clients > 0) issues.push({ level: 'warn', message: '阻塞客户端' });
|
||||
if (m.redis_evicted_keys > 0) issues.push({ level: 'warn', message: 'Key 逐出' });
|
||||
} else if (target.description === 'mysql') {
|
||||
if (m.mysql_buffer_hit_rate > 0 && m.mysql_buffer_hit_rate < 0.9) {
|
||||
issues.push({ level: 'crit', message: '缓冲池命中 < 90%' });
|
||||
} else if (m.mysql_buffer_hit_rate > 0 && m.mysql_buffer_hit_rate < 0.95) {
|
||||
issues.push({ level: 'warn', message: '缓冲池命中 < 95%' });
|
||||
}
|
||||
const connPct = mysqlConnPct(m);
|
||||
if (connPct != null && connPct > 90) issues.push({ level: 'crit', message: '连接 > 90% 上限' });
|
||||
else if (connPct != null && connPct > 80) issues.push({ level: 'warn', message: '连接 > 80% 上限' });
|
||||
if (m.mysql_threads_running > 20) issues.push({ level: 'warn', message: '活跃线程偏多' });
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
export function worstHealthLevel(issues: MwHealthIssue[]): HealthLevel {
|
||||
if (issues.some((i) => i.level === 'crit')) return 'crit';
|
||||
if (issues.some((i) => i.level === 'warn')) return 'warn';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
export function fmtLastSeen(iso?: string): string {
|
||||
if (!iso) return '--';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '--';
|
||||
const diff = Math.floor((Date.now() - d.getTime()) / 1000);
|
||||
if (diff < 60) return `${diff}s 前`;
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m 前`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h 前`;
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
Reference in new issue
Block a user