refactor: 拆分AIAlertAnalyzer.tsx(541→190行)提取analyzerHelpers+AIAnalysisResult+AutoFixDialog子组件
This commit is contained in:
1 parent
35b167a0e2
commit
da5f9a3a07
4 files changed
+427
-377
No files matched your search
@@ -1,16 +1,9 @@
|
||||
// AI 告警智能分析主容器:左侧告警列表 + 右侧 AI 分析结果(已拆分子组件) // Logan.Li
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { alertApi } from '@/services/api';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertTriangle,
|
||||
Brain,
|
||||
@@ -18,202 +11,20 @@ import {
|
||||
XCircle,
|
||||
Clock,
|
||||
Server,
|
||||
Cpu,
|
||||
MemoryStick,
|
||||
HardDrive,
|
||||
Network,
|
||||
Zap,
|
||||
FileText,
|
||||
Terminal,
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
Sparkles,
|
||||
Play,
|
||||
Copy,
|
||||
Check,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface AlertItem {
|
||||
id: string;
|
||||
name: string;
|
||||
severity: 'critical' | 'warning' | 'info';
|
||||
status: 'active' | 'resolved';
|
||||
source: string;
|
||||
metric: string;
|
||||
threshold: string;
|
||||
currentValue: string;
|
||||
duration: string;
|
||||
startedAt: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface AIAnalysis {
|
||||
rootCause: string;
|
||||
impact: string;
|
||||
recommendations: string[];
|
||||
autoFixScript?: string;
|
||||
similarCases: number;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
// ─── Dynamic AI analysis generator ───
|
||||
// Based on alert metric type, generates contextually relevant root cause analysis,
|
||||
// impact assessment, recommendations, and auto-fix scripts.
|
||||
|
||||
function generateAIAnalysis(alert: AlertItem): AIAnalysis {
|
||||
const metric = (alert.metric || '').toLowerCase();
|
||||
const target = alert.source || '目标主机';
|
||||
const isCritical = alert.severity === 'critical';
|
||||
|
||||
const baseConfidence = isCritical ? 88 + Math.floor(Math.random() * 8) : 75 + Math.floor(Math.random() * 12);
|
||||
|
||||
// CPU alerts
|
||||
if (metric.includes('cpu')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 上 CPU 使用率达到 ${alert.currentValue},持续超过阈值 ${alert.threshold}。可能原因:1) 某进程存在死循环或高计算负载;2) GC 频繁触发(Java/Go 应用);3) 业务流量突增导致处理能力不足。`,
|
||||
impact: isCritical
|
||||
? `CPU 持续高负载将导致服务响应延迟显著增加,严重时可能触发雪崩效应,影响全部依赖该节点的业务。`
|
||||
: `CPU 使用率偏高,可能导致部分请求处理变慢,建议及时排查避免进一步恶化。`,
|
||||
recommendations: [
|
||||
`登录 ${target} 执行 top/htop 定位高 CPU 进程`,
|
||||
'检查是否有异常定时任务或批处理任务在运行',
|
||||
`考虑临时扩容或将部分流量调度到其他节点`,
|
||||
'优化应用代码,排查是否存在低效算法或资源泄漏',
|
||||
],
|
||||
autoFixScript: `#!/bin/bash\n# CPU 高负载诊断脚本 - ${target}\necho "===== CPU TOP 10 进程 ====="\nps aux --sort=-%cpu | head -11\necho ""\necho "===== 系统负载 ====="\nuptime\necho ""\necho "===== 高 CPU 进程详情 ====="\nPID=$(ps aux --sort=-%cpu | sed -n '2p' | awk '{print $2}')\nif [ -n "$PID" ]; then\n echo "最高 CPU 进程: PID=$PID"\n cat /proc/$PID/cmdline | tr '\\0' ' '\n echo ""\nfi\n`,
|
||||
similarCases: 18 + Math.floor(Math.random() * 8),
|
||||
confidence: baseConfidence,
|
||||
};
|
||||
}
|
||||
|
||||
// Memory alerts
|
||||
if (metric.includes('mem') || metric.includes('ram')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 内存使用率达到 ${alert.currentValue},接近或超过阈值 ${alert.threshold}。常见原因:1) 应用内存泄漏,对象未被正确释放;2) 缓存配置过大;3) 数据库连接池未及时释放。`,
|
||||
impact: isCritical
|
||||
? `内存即将耗尽可能触发 OOM Killer,导致关键进程被强制终止,服务中断风险极高。`
|
||||
: `内存持续增长,长期可能导致 OOM,建议尽快排查内存使用情况。`,
|
||||
recommendations: [
|
||||
`检查 ${target} 上的内存占用最高的进程`,
|
||||
'如果是 Java 应用,检查 JVM 堆内存配置和 GC 日志',
|
||||
'排查是否存在内存泄漏,必要时生成堆转储分析',
|
||||
'调整缓存大小限制或增加交换分区',
|
||||
],
|
||||
similarCases: 12 + Math.floor(Math.random() * 6),
|
||||
confidence: baseConfidence,
|
||||
};
|
||||
}
|
||||
|
||||
// Disk alerts
|
||||
if (metric.includes('disk') || metric.includes('space')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 磁盘使用率达到 ${alert.currentValue},超过阈值 ${alert.threshold}。最常见原因:1) 日志文件未配置轮转/清理;2) 临时文件堆积;3) 数据库数据文件膨胀。`,
|
||||
impact: isCritical
|
||||
? `磁盘空间不足可能导致服务无法写入数据/日志,数据库可能崩溃,文件系统变只读,影响范围极大。`
|
||||
: `磁盘空间告警,如不及时清理可能导致服务异常。`,
|
||||
recommendations: [
|
||||
`清理 ${target} 上 30 天前的日志文件`,
|
||||
'检查并清理临时目录 /tmp、/var/tmp',
|
||||
'配置 logrotate 日志轮转策略',
|
||||
'考虑扩容磁盘或将数据迁移到对象存储',
|
||||
],
|
||||
autoFixScript: `#!/bin/bash\n# 磁盘清理脚本 - ${target}\necho "===== 清理前磁盘使用 ====="\ndf -h /\necho ""\necho "===== 清理 30 天前日志 ====="\nfind /var/log -name "*.log" -mtime +30 -delete 2>/dev/null\nfind /var/log -name "*.gz" -mtime +30 -delete 2>/dev/null\necho ""\necho "===== 清理系统缓存 ====="\njournalctl --vacuum-time=7d 2>/dev/null\necho ""\necho "===== 清理后磁盘使用 ====="\ndf -h /\n`,
|
||||
similarCases: 23 + Math.floor(Math.random() * 10),
|
||||
confidence: baseConfidence + 3,
|
||||
};
|
||||
}
|
||||
|
||||
// Network alerts
|
||||
if (metric.includes('net') || metric.includes('latency') || metric.includes('bandwidth')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 网络指标异常(当前值 ${alert.currentValue}),可能原因:1) 上游依赖服务响应变慢;2) 网络带宽打满;3) DNS 解析延迟;4) 连接数达到上限。`,
|
||||
impact: isCritical
|
||||
? `网络异常将导致所有经过该节点的请求超时,影响所有上游业务的可用性。`
|
||||
: `网络延迟偏高,可能影响用户体验,建议排查网络链路。`,
|
||||
recommendations: [
|
||||
`检查 ${target} 的网络连接和带宽使用情况`,
|
||||
'排查上游依赖服务的健康状态和响应时间',
|
||||
'检查防火墙规则是否有限速或异常丢弃',
|
||||
'考虑启用熔断/降级机制保障核心链路',
|
||||
],
|
||||
similarCases: 8 + Math.floor(Math.random() * 5),
|
||||
confidence: baseConfidence - 5,
|
||||
};
|
||||
}
|
||||
|
||||
// Service availability alerts
|
||||
if (metric.includes('service') || metric.includes('up') || metric.includes('avail')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 服务不可用(指标 ${alert.metric} = ${alert.currentValue})。可能原因:1) 进程崩溃或被 OOM Killer 终止;2) 端口被占用或监听异常;3) 健康检查端点返回错误;4) 配置文件错误导致启动失败。`,
|
||||
impact: isCritical
|
||||
? `服务完全不可用,所有依赖该服务的业务功能将中断,需要立即处理。`
|
||||
: `服务可用性下降,部分功能可能受影响。`,
|
||||
recommendations: [
|
||||
`立即检查 ${target} 的服务状态: systemctl status <service>`,
|
||||
'查看服务日志排查崩溃原因',
|
||||
'检查端口占用和监听状态: ss -tlnp',
|
||||
'必要时重启服务并监控恢复情况',
|
||||
],
|
||||
autoFixScript: `#!/bin/bash\n# 服务恢复脚本 - ${target}\necho "===== 检查服务状态 ====="\nsystemctl list-units --state=failed\necho ""\necho "===== 检查端口监听 ====="\nss -tlnp | grep -E ':(80|443|8080|3306|6379|9090)'\necho ""\necho "===== 重启失败服务 ====="\nsystemctl reset-failed\n`,
|
||||
similarCases: 6 + Math.floor(Math.random() * 4),
|
||||
confidence: baseConfidence,
|
||||
};
|
||||
}
|
||||
|
||||
// Default / generic analysis
|
||||
return {
|
||||
rootCause: `检测到 ${target} 触发告警「${alert.name}」,当前指标值 ${alert.currentValue} 超过阈值 ${alert.threshold}。建议进一步排查具体指标数据和相关日志以确定根因。`,
|
||||
impact: isCritical
|
||||
? `该告警级别为严重,可能影响业务正常运行,建议优先处理。`
|
||||
: `该告警需要关注,持续恶化可能影响系统稳定性。`,
|
||||
recommendations: [
|
||||
`查看 ${target} 的监控仪表盘,确认指标趋势`,
|
||||
'检查对应时间段的系统日志和应用日志',
|
||||
'关联同时间段的其他告警,排查是否有连锁反应',
|
||||
'若确认误报,调整告警规则阈值或持续时间',
|
||||
],
|
||||
similarCases: 5 + Math.floor(Math.random() * 5),
|
||||
confidence: baseConfidence - 8,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Backend alert event → AlertItem mapping ───
|
||||
function mapAlertEvent(event: any): AlertItem {
|
||||
const expr = event.rule?.expression || '';
|
||||
// Extract metric name and threshold from expression like "cpu_usage > 80"
|
||||
const metricMatch = expr.match(/([\w_]+)\s*[><=!]+\s*([\d.]+)/);
|
||||
const metric = metricMatch ? metricMatch[1] : expr || 'unknown';
|
||||
const threshold = metricMatch ? metricMatch[2] + '%' : '-';
|
||||
|
||||
const value = event.value ?? 0;
|
||||
const isPercent = metric.includes('usage') || metric.includes('cpu') || metric.includes('disk') || metric.includes('mem');
|
||||
const currentValue = isPercent ? value.toFixed(1) + '%' : value.toFixed(2);
|
||||
|
||||
// Compute duration from started_at to now (or resolved_at)
|
||||
const endTime = event.resolved_at ? new Date(event.resolved_at) : new Date();
|
||||
const startTime = new Date(event.started_at);
|
||||
const diffMin = Math.max(1, Math.round((endTime.getTime() - startTime.getTime()) / 60000));
|
||||
const duration = diffMin >= 60 ? `${Math.floor(diffMin / 60)}小时${diffMin % 60}分钟` : `${diffMin}分钟`;
|
||||
|
||||
const startedAt = startTime.toLocaleString('zh-CN', { hour12: false });
|
||||
|
||||
return {
|
||||
id: String(event.id),
|
||||
name: event.rule_name || event.rule?.name || '未知告警',
|
||||
severity: event.severity === 'critical' ? 'critical' : event.severity === 'warning' ? 'warning' : 'info',
|
||||
status: event.status === 'firing' ? 'active' : 'resolved',
|
||||
source: event.target || '未知目标',
|
||||
metric,
|
||||
threshold,
|
||||
currentValue,
|
||||
duration,
|
||||
startedAt,
|
||||
description: event.message && event.message !== `告警事件 ${event.id}`
|
||||
? event.message
|
||||
: event.rule?.description || `${event.rule_name || '告警'}: ${event.target} 当前值 ${currentValue}`,
|
||||
};
|
||||
}
|
||||
import {
|
||||
type AlertItem,
|
||||
type AIAnalysis,
|
||||
generateAIAnalysis,
|
||||
mapAlertEvent,
|
||||
getSeverityColor,
|
||||
} from './analyzerHelpers';
|
||||
import { AIAnalysisResult } from './AIAnalysisResult';
|
||||
import { AutoFixDialog } from './AutoFixDialog';
|
||||
|
||||
export function AIAlertAnalyzer() {
|
||||
const [alerts, setAlerts] = useState<AlertItem[]>([]);
|
||||
@@ -222,7 +33,6 @@ export function AIAlertAnalyzer() {
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [analysis, setAnalysis] = useState<AIAnalysis | null>(null);
|
||||
const [showScriptDialog, setShowScriptDialog] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadAlerts();
|
||||
@@ -232,6 +42,7 @@ export function AIAlertAnalyzer() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await alertApi.getAlerts({ page: 1, page_size: 50 });
|
||||
// 列表响应格式:http.ts 已提取 .data,此处仍是 {list, total}
|
||||
const list = res?.list || res || [];
|
||||
setAlerts(list.map(mapAlertEvent));
|
||||
} catch (e) {
|
||||
@@ -242,12 +53,12 @@ export function AIAlertAnalyzer() {
|
||||
}
|
||||
};
|
||||
|
||||
// 触发 AI 分析(模拟 1.5s 思考,实际由 generateAIAnalysis 动态生成上下文相关结果)
|
||||
const handleAnalyze = (alert: AlertItem) => {
|
||||
setSelectedAlert(alert);
|
||||
setIsAnalyzing(true);
|
||||
setAnalysis(null);
|
||||
|
||||
// Simulate AI analysis with dynamic generation
|
||||
setTimeout(() => {
|
||||
setAnalysis(generateAIAnalysis(alert));
|
||||
setIsAnalyzing(false);
|
||||
@@ -258,25 +69,7 @@ export function AIAlertAnalyzer() {
|
||||
setShowScriptDialog(true);
|
||||
};
|
||||
|
||||
const copyScript = () => {
|
||||
if (analysis?.autoFixScript) {
|
||||
navigator.clipboard.writeText(analysis.autoFixScript);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return 'bg-red-500/10 text-red-400 border-red-500/30';
|
||||
case 'warning':
|
||||
return 'bg-amber-500/10 text-amber-400 border-amber-500/30';
|
||||
default:
|
||||
return 'bg-blue-500/10 text-blue-400 border-blue-500/30';
|
||||
}
|
||||
};
|
||||
|
||||
// 严重等级图标(返回 JSX,保留在主组件中)
|
||||
const getSeverityIcon = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
@@ -310,7 +103,7 @@ export function AIAlertAnalyzer() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 h-[calc(100vh-200px)]">
|
||||
{/* Alert List */}
|
||||
{/* 告警列表(左侧) */}
|
||||
<Card className="bg-slate-800 border-slate-700 flex flex-col">
|
||||
<CardHeader className="border-b border-slate-700 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -377,165 +170,21 @@ export function AIAlertAnalyzer() {
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
|
||||
{/* AI Analysis Result */}
|
||||
<Card className="bg-slate-800 border-slate-700 flex flex-col">
|
||||
<CardHeader className="border-b border-slate-700 pb-3">
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-purple-400" />
|
||||
AI 分析结果
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<ScrollArea className="flex-1">
|
||||
<CardContent className="p-4">
|
||||
{!selectedAlert ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-slate-500">
|
||||
<Brain className="h-16 w-16 mb-4 opacity-30" />
|
||||
<p>选择左侧告警进行AI智能分析</p>
|
||||
</div>
|
||||
) : isAnalyzing ? (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-purple-400 mb-4" />
|
||||
<p className="text-slate-400">AI正在分析告警根因...</p>
|
||||
<p className="text-xs text-slate-500 mt-2">正在关联日志、指标和历史案例</p>
|
||||
</div>
|
||||
) : analysis ? (
|
||||
<div className="space-y-4">
|
||||
{/* Confidence Score */}
|
||||
<div className="flex items-center justify-between p-3 bg-purple-500/10 rounded-lg border border-purple-500/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="h-5 w-5 text-purple-400" />
|
||||
<span className="text-slate-300">分析置信度</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-32 h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-purple-500 to-pink-500"
|
||||
style={{ width: `${analysis.confidence}%` }}
|
||||
{/* AI 分析结果(右侧,子组件) */}
|
||||
<AIAnalysisResult
|
||||
selectedAlert={selectedAlert}
|
||||
isAnalyzing={isAnalyzing}
|
||||
analysis={analysis}
|
||||
onAutoFix={handleAutoFix}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-purple-400 font-bold">{analysis.confidence}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Root Cause */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-300 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-400" />
|
||||
根因分析
|
||||
</h4>
|
||||
<div className="p-3 bg-slate-900 rounded-lg border border-slate-700">
|
||||
<p className="text-sm text-slate-300 leading-relaxed">{analysis.rootCause}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Impact */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-300 flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4 text-amber-400" />
|
||||
影响范围
|
||||
</h4>
|
||||
<div className="p-3 bg-slate-900 rounded-lg border border-slate-700">
|
||||
<p className="text-sm text-slate-300 leading-relaxed">{analysis.impact}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recommendations */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-300 flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-emerald-400" />
|
||||
修复建议
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{analysis.recommendations.map((rec, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-start gap-2 p-2 bg-slate-900 rounded-lg border border-slate-700"
|
||||
>
|
||||
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center text-xs">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span className="text-sm text-slate-300">{rec}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Similar Cases */}
|
||||
<div className="flex items-center gap-2 p-3 bg-slate-900 rounded-lg border border-slate-700">
|
||||
<FileText className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-sm text-slate-400">
|
||||
发现 <span className="text-blue-400 font-medium">{analysis.similarCases}</span> 个相似历史案例
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Auto Fix */}
|
||||
{analysis.autoFixScript && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-300 flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-purple-400" />
|
||||
自动修复脚本
|
||||
</h4>
|
||||
<div className="p-3 bg-slate-950 rounded-lg border border-slate-700">
|
||||
<pre className="text-xs text-slate-400 overflow-x-auto">{analysis.autoFixScript}</pre>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full bg-purple-600 hover:bg-purple-700"
|
||||
onClick={handleAutoFix}
|
||||
>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
执行自动修复
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Auto Fix Dialog */}
|
||||
<Dialog open={showScriptDialog} onOpenChange={setShowScriptDialog}>
|
||||
<DialogContent className="max-w-2xl bg-slate-900 border-slate-700 text-slate-100">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-purple-400" />
|
||||
自动修复脚本
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
AI生成的自动修复脚本,请确认后执行
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="p-4 bg-slate-950 rounded-lg border border-slate-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-slate-400">修复脚本</span>
|
||||
<Button variant="ghost" size="sm" onClick={copyScript} className="text-slate-400">
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="text-sm text-slate-300 overflow-x-auto whitespace-pre-wrap">
|
||||
{analysis?.autoFixScript}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-3 bg-amber-500/10 rounded-lg border border-amber-500/30">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-400" />
|
||||
<span className="text-sm text-amber-400">
|
||||
警告:执行脚本前请确保已备份重要数据
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1 border-slate-700 text-slate-300" onClick={() => setShowScriptDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="flex-1 bg-purple-600 hover:bg-purple-700" onClick={() => setShowScriptDialog(false)}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
确认执行
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* 自动修复脚本确认弹窗(子组件) */}
|
||||
<AutoFixDialog
|
||||
open={showScriptDialog}
|
||||
onOpenChange={setShowScriptDialog}
|
||||
script={analysis?.autoFixScript}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// AI 分析结果卡片(右侧面板):置信度/根因/影响/修复建议/相似案例/自动修复脚本 // Logan.Li
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Sparkles, Brain, Loader2 } from 'lucide-react';
|
||||
import { AlertCircle, TrendingUp, CheckCircle, FileText, Terminal, Play } from 'lucide-react';
|
||||
import type { AlertItem, AIAnalysis } from './analyzerHelpers';
|
||||
|
||||
interface AIAnalysisResultProps {
|
||||
selectedAlert: AlertItem | null;
|
||||
isAnalyzing: boolean;
|
||||
analysis: AIAnalysis | null;
|
||||
onAutoFix: () => void;
|
||||
}
|
||||
|
||||
export function AIAnalysisResult({ selectedAlert, isAnalyzing, analysis, onAutoFix }: AIAnalysisResultProps) {
|
||||
return (
|
||||
<Card className="bg-slate-800 border-slate-700 flex flex-col">
|
||||
<CardHeader className="border-b border-slate-700 pb-3">
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-purple-400" />
|
||||
AI 分析结果
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<ScrollArea className="flex-1">
|
||||
<CardContent className="p-4">
|
||||
{!selectedAlert ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-slate-500">
|
||||
<Brain className="h-16 w-16 mb-4 opacity-30" />
|
||||
<p>选择左侧告警进行AI智能分析</p>
|
||||
</div>
|
||||
) : isAnalyzing ? (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-purple-400 mb-4" />
|
||||
<p className="text-slate-400">AI正在分析告警根因...</p>
|
||||
<p className="text-xs text-slate-500 mt-2">正在关联日志、指标和历史案例</p>
|
||||
</div>
|
||||
) : analysis ? (
|
||||
<div className="space-y-4">
|
||||
{/* 置信度 */}
|
||||
<div className="flex items-center justify-between p-3 bg-purple-500/10 rounded-lg border border-purple-500/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="h-5 w-5 text-purple-400" />
|
||||
<span className="text-slate-300">分析置信度</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-32 h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-purple-500 to-pink-500"
|
||||
style={{ width: `${analysis.confidence}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-purple-400 font-bold">{analysis.confidence}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 根因分析 */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-300 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-400" />
|
||||
根因分析
|
||||
</h4>
|
||||
<div className="p-3 bg-slate-900 rounded-lg border border-slate-700">
|
||||
<p className="text-sm text-slate-300 leading-relaxed">{analysis.rootCause}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 影响范围 */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-300 flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4 text-amber-400" />
|
||||
影响范围
|
||||
</h4>
|
||||
<div className="p-3 bg-slate-900 rounded-lg border border-slate-700">
|
||||
<p className="text-sm text-slate-300 leading-relaxed">{analysis.impact}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 修复建议 */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-300 flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-emerald-400" />
|
||||
修复建议
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{analysis.recommendations.map((rec, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-start gap-2 p-2 bg-slate-900 rounded-lg border border-slate-700"
|
||||
>
|
||||
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center text-xs">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span className="text-sm text-slate-300">{rec}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 相似历史案例 */}
|
||||
<div className="flex items-center gap-2 p-3 bg-slate-900 rounded-lg border border-slate-700">
|
||||
<FileText className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-sm text-slate-400">
|
||||
发现 <span className="text-blue-400 font-medium">{analysis.similarCases}</span> 个相似历史案例
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 自动修复脚本 */}
|
||||
{analysis.autoFixScript && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-300 flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-purple-400" />
|
||||
自动修复脚本
|
||||
</h4>
|
||||
<div className="p-3 bg-slate-950 rounded-lg border border-slate-700">
|
||||
<pre className="text-xs text-slate-400 overflow-x-auto">{analysis.autoFixScript}</pre>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full bg-purple-600 hover:bg-purple-700"
|
||||
onClick={onAutoFix}
|
||||
>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
执行自动修复
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 自动修复脚本确认弹窗:展示 AI 生成的修复脚本 + 复制按钮 + 确认执行 // Logan.Li
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { AlertTriangle, Copy, Check, Play, Terminal } from 'lucide-react';
|
||||
|
||||
interface AutoFixDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
script?: string;
|
||||
}
|
||||
|
||||
export function AutoFixDialog({ open, onOpenChange, script }: AutoFixDialogProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyScript = () => {
|
||||
if (script) {
|
||||
navigator.clipboard.writeText(script);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl bg-slate-900 border-slate-700 text-slate-100">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-purple-400" />
|
||||
自动修复脚本
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
AI生成的自动修复脚本,请确认后执行
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="p-4 bg-slate-950 rounded-lg border border-slate-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-slate-400">修复脚本</span>
|
||||
<Button variant="ghost" size="sm" onClick={copyScript} className="text-slate-400">
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="text-sm text-slate-300 overflow-x-auto whitespace-pre-wrap">
|
||||
{script}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-3 bg-amber-500/10 rounded-lg border border-amber-500/30">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-400" />
|
||||
<span className="text-sm text-amber-400">
|
||||
警告:执行脚本前请确保已备份重要数据
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1 border-slate-700 text-slate-300" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="flex-1 bg-purple-600 hover:bg-purple-700" onClick={() => onOpenChange(false)}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
确认执行
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// AI 告警分析纯逻辑辅助函数:类型定义 + 后端告警映射 + 动态 AI 分析生成 + 严重等级配色 // Logan.Li
|
||||
|
||||
// 告警条目(前端统一模型,从后端 alert event 映射而来)
|
||||
export interface AlertItem {
|
||||
id: string;
|
||||
name: string;
|
||||
severity: 'critical' | 'warning' | 'info';
|
||||
status: 'active' | 'resolved';
|
||||
source: string;
|
||||
metric: string;
|
||||
threshold: string;
|
||||
currentValue: string;
|
||||
duration: string;
|
||||
startedAt: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// AI 分析结果
|
||||
export interface AIAnalysis {
|
||||
rootCause: string;
|
||||
impact: string;
|
||||
recommendations: string[];
|
||||
autoFixScript?: string;
|
||||
similarCases: number;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
// ─── Dynamic AI analysis generator ───
|
||||
// 基于告警指标类型,生成上下文相关的根因分析、影响评估、修复建议与自动修复脚本。
|
||||
export function generateAIAnalysis(alert: AlertItem): AIAnalysis {
|
||||
const metric = (alert.metric || '').toLowerCase();
|
||||
const target = alert.source || '目标主机';
|
||||
const isCritical = alert.severity === 'critical';
|
||||
|
||||
const baseConfidence = isCritical ? 88 + Math.floor(Math.random() * 8) : 75 + Math.floor(Math.random() * 12);
|
||||
|
||||
// CPU 告警
|
||||
if (metric.includes('cpu')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 上 CPU 使用率达到 ${alert.currentValue},持续超过阈值 ${alert.threshold}。可能原因:1) 某进程存在死循环或高计算负载;2) GC 频繁触发(Java/Go 应用);3) 业务流量突增导致处理能力不足。`,
|
||||
impact: isCritical
|
||||
? `CPU 持续高负载将导致服务响应延迟显著增加,严重时可能触发雪崩效应,影响全部依赖该节点的业务。`
|
||||
: `CPU 使用率偏高,可能导致部分请求处理变慢,建议及时排查避免进一步恶化。`,
|
||||
recommendations: [
|
||||
`登录 ${target} 执行 top/htop 定位高 CPU 进程`,
|
||||
'检查是否有异常定时任务或批处理任务在运行',
|
||||
`考虑临时扩容或将部分流量调度到其他节点`,
|
||||
'优化应用代码,排查是否存在低效算法或资源泄漏',
|
||||
],
|
||||
autoFixScript: `#!/bin/bash\n# CPU 高负载诊断脚本 - ${target}\necho "===== CPU TOP 10 进程 ====="\nps aux --sort=-%cpu | head -11\necho ""\necho "===== 系统负载 ====="\nuptime\necho ""\necho "===== 高 CPU 进程详情 ====="\nPID=$(ps aux --sort=-%cpu | sed -n '2p' | awk '{print $2}')\nif [ -n "$PID" ]; then\n echo "最高 CPU 进程: PID=$PID"\n cat /proc/$PID/cmdline | tr '\\0' ' '\n echo ""\nfi\n`,
|
||||
similarCases: 18 + Math.floor(Math.random() * 8),
|
||||
confidence: baseConfidence,
|
||||
};
|
||||
}
|
||||
|
||||
// 内存告警
|
||||
if (metric.includes('mem') || metric.includes('ram')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 内存使用率达到 ${alert.currentValue},接近或超过阈值 ${alert.threshold}。常见原因:1) 应用内存泄漏,对象未被正确释放;2) 缓存配置过大;3) 数据库连接池未及时释放。`,
|
||||
impact: isCritical
|
||||
? `内存即将耗尽可能触发 OOM Killer,导致关键进程被强制终止,服务中断风险极高。`
|
||||
: `内存持续增长,长期可能导致 OOM,建议尽快排查内存使用情况。`,
|
||||
recommendations: [
|
||||
`检查 ${target} 上的内存占用最高的进程`,
|
||||
'如果是 Java 应用,检查 JVM 堆内存配置和 GC 日志',
|
||||
'排查是否存在内存泄漏,必要时生成堆转储分析',
|
||||
'调整缓存大小限制或增加交换分区',
|
||||
],
|
||||
similarCases: 12 + Math.floor(Math.random() * 6),
|
||||
confidence: baseConfidence,
|
||||
};
|
||||
}
|
||||
|
||||
// 磁盘告警
|
||||
if (metric.includes('disk') || metric.includes('space')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 磁盘使用率达到 ${alert.currentValue},超过阈值 ${alert.threshold}。最常见原因:1) 日志文件未配置轮转/清理;2) 临时文件堆积;3) 数据库数据文件膨胀。`,
|
||||
impact: isCritical
|
||||
? `磁盘空间不足可能导致服务无法写入数据/日志,数据库可能崩溃,文件系统变只读,影响范围极大。`
|
||||
: `磁盘空间告警,如不及时清理可能导致服务异常。`,
|
||||
recommendations: [
|
||||
`清理 ${target} 上 30 天前的日志文件`,
|
||||
'检查并清理临时目录 /tmp、/var/tmp',
|
||||
'配置 logrotate 日志轮转策略',
|
||||
'考虑扩容磁盘或将数据迁移到对象存储',
|
||||
],
|
||||
autoFixScript: `#!/bin/bash\n# 磁盘清理脚本 - ${target}\necho "===== 清理前磁盘使用 ====="\ndf -h /\necho ""\necho "===== 清理 30 天前日志 ====="\nfind /var/log -name "*.log" -mtime +30 -delete 2>/dev/null\nfind /var/log -name "*.gz" -mtime +30 -delete 2>/dev/null\necho ""\necho "===== 清理系统缓存 ====="\njournalctl --vacuum-time=7d 2>/dev/null\necho ""\necho "===== 清理后磁盘使用 ====="\ndf -h /\n`,
|
||||
similarCases: 23 + Math.floor(Math.random() * 10),
|
||||
confidence: baseConfidence + 3,
|
||||
};
|
||||
}
|
||||
|
||||
// 网络告警
|
||||
if (metric.includes('net') || metric.includes('latency') || metric.includes('bandwidth')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 网络指标异常(当前值 ${alert.currentValue}),可能原因:1) 上游依赖服务响应变慢;2) 网络带宽打满;3) DNS 解析延迟;4) 连接数达到上限。`,
|
||||
impact: isCritical
|
||||
? `网络异常将导致所有经过该节点的请求超时,影响所有上游业务的可用性。`
|
||||
: `网络延迟偏高,可能影响用户体验,建议排查网络链路。`,
|
||||
recommendations: [
|
||||
`检查 ${target} 的网络连接和带宽使用情况`,
|
||||
'排查上游依赖服务的健康状态和响应时间',
|
||||
'检查防火墙规则是否有限速或异常丢弃',
|
||||
'考虑启用熔断/降级机制保障核心链路',
|
||||
],
|
||||
similarCases: 8 + Math.floor(Math.random() * 5),
|
||||
confidence: baseConfidence - 5,
|
||||
};
|
||||
}
|
||||
|
||||
// 服务可用性告警
|
||||
if (metric.includes('service') || metric.includes('up') || metric.includes('avail')) {
|
||||
return {
|
||||
rootCause: `检测到 ${target} 服务不可用(指标 ${alert.metric} = ${alert.currentValue})。可能原因:1) 进程崩溃或被 OOM Killer 终止;2) 端口被占用或监听异常;3) 健康检查端点返回错误;4) 配置文件错误导致启动失败。`,
|
||||
impact: isCritical
|
||||
? `服务完全不可用,所有依赖该服务的业务功能将中断,需要立即处理。`
|
||||
: `服务可用性下降,部分功能可能受影响。`,
|
||||
recommendations: [
|
||||
`立即检查 ${target} 的服务状态: systemctl status <service>`,
|
||||
'查看服务日志排查崩溃原因',
|
||||
'检查端口占用和监听状态: ss -tlnp',
|
||||
'必要时重启服务并监控恢复情况',
|
||||
],
|
||||
autoFixScript: `#!/bin/bash\n# 服务恢复脚本 - ${target}\necho "===== 检查服务状态 ====="\nsystemctl list-units --state=failed\necho ""\necho "===== 检查端口监听 ====="\nss -tlnp | grep -E ':(80|443|8080|3306|6379|9090)'\necho ""\necho "===== 重启失败服务 ====="\nsystemctl reset-failed\n`,
|
||||
similarCases: 6 + Math.floor(Math.random() * 4),
|
||||
confidence: baseConfidence,
|
||||
};
|
||||
}
|
||||
|
||||
// 默认/通用分析
|
||||
return {
|
||||
rootCause: `检测到 ${target} 触发告警「${alert.name}」,当前指标值 ${alert.currentValue} 超过阈值 ${alert.threshold}。建议进一步排查具体指标数据和相关日志以确定根因。`,
|
||||
impact: isCritical
|
||||
? `该告警级别为严重,可能影响业务正常运行,建议优先处理。`
|
||||
: `该告警需要关注,持续恶化可能影响系统稳定性。`,
|
||||
recommendations: [
|
||||
`查看 ${target} 的监控仪表盘,确认指标趋势`,
|
||||
'检查对应时间段的系统日志和应用日志',
|
||||
'关联同时间段的其他告警,排查是否有连锁反应',
|
||||
'若确认误报,调整告警规则阈值或持续时间',
|
||||
],
|
||||
similarCases: 5 + Math.floor(Math.random() * 5),
|
||||
confidence: baseConfidence - 8,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 后端告警事件 → AlertItem 映射 ───
|
||||
// 后端 alert event 字段为 snake_case(rule_name/target/started_at/value 等),此处统一映射为前端 AlertItem
|
||||
export function mapAlertEvent(event: any): AlertItem {
|
||||
const expr = event.rule?.expression || '';
|
||||
// 从表达式(如 "cpu_usage > 80")中提取指标名与阈值
|
||||
const metricMatch = expr.match(/([\w_]+)\s*[><=!]+\s*([\d.]+)/);
|
||||
const metric = metricMatch ? metricMatch[1] : expr || 'unknown';
|
||||
const threshold = metricMatch ? metricMatch[2] + '%' : '-';
|
||||
|
||||
const value = event.value ?? 0;
|
||||
const isPercent = metric.includes('usage') || metric.includes('cpu') || metric.includes('disk') || metric.includes('mem');
|
||||
const currentValue = isPercent ? value.toFixed(1) + '%' : value.toFixed(2);
|
||||
|
||||
// 从 started_at 到现在(或 resolved_at)计算持续时间
|
||||
const endTime = event.resolved_at ? new Date(event.resolved_at) : new Date();
|
||||
const startTime = new Date(event.started_at);
|
||||
const diffMin = Math.max(1, Math.round((endTime.getTime() - startTime.getTime()) / 60000));
|
||||
const duration = diffMin >= 60 ? `${Math.floor(diffMin / 60)}小时${diffMin % 60}分钟` : `${diffMin}分钟`;
|
||||
|
||||
const startedAt = startTime.toLocaleString('zh-CN', { hour12: false });
|
||||
|
||||
return {
|
||||
id: String(event.id),
|
||||
name: event.rule_name || event.rule?.name || '未知告警',
|
||||
severity: event.severity === 'critical' ? 'critical' : event.severity === 'warning' ? 'warning' : 'info',
|
||||
status: event.status === 'firing' ? 'active' : 'resolved',
|
||||
source: event.target || '未知目标',
|
||||
metric,
|
||||
threshold,
|
||||
currentValue,
|
||||
duration,
|
||||
startedAt,
|
||||
description: event.message && event.message !== `告警事件 ${event.id}`
|
||||
? event.message
|
||||
: event.rule?.description || `${event.rule_name || '告警'}: ${event.target} 当前值 ${currentValue}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 严重等级配色(纯字符串,无 JSX)
|
||||
export function getSeverityColor(severity: string): string {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return 'bg-red-500/10 text-red-400 border-red-500/30';
|
||||
case 'warning':
|
||||
return 'bg-amber-500/10 text-amber-400 border-amber-500/30';
|
||||
default:
|
||||
return 'bg-blue-500/10 text-blue-400 border-blue-500/30';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user