refactor: 拆分AIPredictiveAnalytics.tsx(670→417行)提取predictUtils子模块

This commit is contained in:
attacker committed 2026-06-16 06:05:44 +08:00
1 parent 67066041b3
commit 11c97cd885
3 files changed
+339 -272

No files matched your search

@@ -20,143 +20,25 @@ import {
Cpu,
MemoryStick,
Network,
Database,
Activity,
BarChart3,
LineChart,
Calendar,
Sparkles,
Zap,
ArrowUpRight,
Minus,
Loader2,
} from 'lucide-react';
import { dashboardStatsApi, serverApi, alertApi } from '@/services/api';
import { PredictiveCapacityTab } from './PredictiveCapacityTab';
import {
computePredictions,
trendColorClass,
type CapacityForecast,
} from './predictUtils';
interface PredictionMetric {
name: string;
current: number;
predicted: number;
unit: string;
trend: 'up' | 'down' | 'stable';
confidence: number;
timeRange: string;
}
export interface CapacityForecast {
resource: string;
metricKey: string;
currentUsage: number;
predictedUsage: number;
threshold: number;
daysUntilThreshold: number;
recommendation: string;
}
interface AnomalyPrediction {
metric: string;
predictedAnomaly: string;
probability: number;
timeframe: string;
suggestedAction: string;
}
/** 指标定义:后端 metric name → 展示标签 / 告警表达式前缀 / 阈值 */
const METRIC_DEFS = [
{ key: 'cpu', label: 'CPU使用率', exprKey: 'cpu_usage', threshold: 85 },
{ key: 'memory', label: '内存使用率', exprKey: 'memory_usage', threshold: 85 },
{ key: 'disk', label: '磁盘使用率', exprKey: 'disk_usage', threshold: 90 },
] as const;
const TIME_RANGE_LABELS: Record<string, string> = {
'1h': '1小时后',
'24h': '24小时后',
'7d': '7天后',
'30d': '30天后',
};
const TIME_RANGE_HOURS: Record<string, number> = {
'1h': 1,
'24h': 24,
'7d': 168,
'30d': 720,
};
/** 简单最小二乘线性回归,返回斜率和 R² */
function linearRegression(values: number[]): { slope: number; r2: number } {
const n = values.length;
if (n < 2) return { slope: 0, r2: 0 };
const xs = values.map((_, i) => i);
const meanX = xs.reduce((a, b) => a + b, 0) / n;
const meanY = values.reduce((a, b) => a + b, 0) / n;
let num = 0,
den = 0,
ssTot = 0,
ssRes = 0;
for (let i = 0; i < n; i++) {
num += (xs[i] - meanX) * (values[i] - meanY);
den += (xs[i] - meanX) ** 2;
ssTot += (values[i] - meanY) ** 2;
}
const slope = den === 0 ? 0 : num / den;
const intercept = meanY - slope * meanX;
for (let i = 0; i < n; i++) {
const pred = intercept + slope * xs[i];
ssRes += (values[i] - pred) ** 2;
}
const r2 = ssTot === 0 ? 1 : Math.max(0, 1 - ssRes / ssTot);
return { slope, r2 };
}
/** 从时序指标中提取按时间排序的数值序列 */
function extractSeries(metrics: any[], name: string): number[] {
return metrics
.filter((m) => m.name === name)
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
.map((m) => Number(m.value) || 0);
}
/** 从告警表达式中解析阈值,如 "cpu_usage > 80" → 80 */
function parseThreshold(expr: string): number | null {
const m = expr.match(/[><=]\s*([\d.]+)/);
return m ? parseFloat(m[1]) : null;
}
/** 容量规划建议 */
function capacityRecommendation(metricKey: string, days: number): string {
const level = days <= 7 ? 'urgent' : days <= 14 ? 'warning' : 'normal';
const recs: Record<string, Record<string, string>> = {
urgent: {
cpu: 'CPU增长趋势明显,建议尽快扩容计算资源或优化高负载进程',
memory: '内存增长趋势明显,建议增加内存或排查内存泄漏',
disk: '磁盘空间即将耗尽,建议立即扩容或清理历史数据与日志',
},
warning: {
cpu: 'CPU使用率呈上升趋势,建议持续监控并准备扩容计划',
memory: '内存使用率呈上升趋势,建议优化应用内存使用',
disk: '磁盘使用率持续增长,建议规划数据归档和清理策略',
},
normal: {
cpu: 'CPU使用正常,建议持续监控',
memory: '内存使用正常,建议持续监控',
disk: '磁盘空间充足,建议持续监控',
},
};
return recs[level]?.[metricKey] || '建议持续监控资源使用趋势';
}
/** 异常建议操作 */
function anomalyAction(metricKey: string): string {
const actions: Record<string, string> = {
cpu: '检查高负载进程,优化CPU密集型任务,考虑横向扩容',
memory: '排查内存泄漏,优化进程内存配置,考虑增加节点',
disk: '清理历史数据和日志,实施数据归档策略,扩容磁盘',
service: '检查服务健康状态与依赖组件,查看服务日志',
pod: '检查Pod重启原因,查看容器日志,排查OOM或健康检查失败',
};
return actions[metricKey] || '建议检查相关服务状态并持续监控';
}
// 重新导出 CapacityForecast,兼容已有 import 路径 './AIPredictiveAnalytics'
export type { CapacityForecast };
export function AIPredictiveAnalytics() {
const [selectedTimeRange, setSelectedTimeRange] = useState('24h');
@@ -201,139 +83,15 @@ export function AIPredictiveAnalytics() {
setIsAnalyzing(false);
};
// ============ 计算指标预测 ============
const horizonHours = TIME_RANGE_HOURS[selectedTimeRange] || 24;
const timeRangeLabel = TIME_RANGE_LABELS[selectedTimeRange] || '24小时后';
const predictions: PredictionMetric[] = METRIC_DEFS.map((def) => {
const series = extractSeries(rawMetrics, def.key);
const { slope, r2 } = linearRegression(series);
const avgFallback =
def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk;
const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0;
let predicted = current + slope * horizonHours;
predicted = Math.max(0, Math.min(100, predicted));
const trend: 'up' | 'down' | 'stable' = slope > 0.05 ? 'up' : slope < -0.05 ? 'down' : 'stable';
const confidence = Math.round(60 + r2 * 35);
return {
name: def.label,
current: Math.round(current),
predicted: Math.round(predicted),
unit: '%',
trend,
confidence,
timeRange: timeRangeLabel,
};
});
// ============ 计算容量规划 ============
const capacityForecasts: CapacityForecast[] = METRIC_DEFS.map((def) => {
const series = extractSeries(rawMetrics, def.key);
const { slope } = linearRegression(series);
const avgFallback =
def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk;
const currentUsage = Math.round(series.length > 0 ? series[series.length - 1] : avgFallback || 0);
const slopePerDay = slope * 24;
const predictedUsage = Math.round(Math.min(100, currentUsage + slopePerDay * 7));
const threshold = def.threshold;
let daysUntilThreshold = 999;
if (slopePerDay > 0.01) {
daysUntilThreshold = Math.round((threshold - currentUsage) / slopePerDay);
daysUntilThreshold = daysUntilThreshold < 1 ? 1 : daysUntilThreshold;
}
const resourceNames: Record<string, string> = {
cpu: '服务器CPU算力',
memory: '服务器内存',
disk: '存储磁盘空间',
};
return {
resource: resourceNames[def.key] || def.label,
metricKey: def.key,
currentUsage,
predictedUsage,
threshold,
daysUntilThreshold,
recommendation: capacityRecommendation(def.key, daysUntilThreshold),
};
}).filter((f) => f.currentUsage > 0);
// ============ 计算异常预测 ============
const anomalyPredictions: AnomalyPrediction[] = [];
// 基于 metric 告警规则 + 当前趋势计算异常概率
for (const rule of alertRules) {
if (!rule.enabled) continue;
const threshold = parseThreshold(rule.expression || '');
const exprLower = (rule.expression || '').toLowerCase();
const matchedDef = METRIC_DEFS.find((def) => exprLower.includes(def.exprKey));
if (matchedDef && threshold) {
const series = extractSeries(rawMetrics, matchedDef.key);
const { slope } = linearRegression(series);
const avgFallback =
matchedDef.key === 'cpu'
? serverStats.avg_cpu
: matchedDef.key === 'memory'
? serverStats.avg_memory
: serverStats.avg_disk;
const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0;
const ratio = threshold > 0 ? current / threshold : 0;
let probability = Math.round(ratio * 55);
if (slope > 0.05) probability += 15;
const eventCount = alertEvents.filter(
(a) => a.rule_id === rule.id || a.rule_name === rule.name,
).length;
probability += Math.min(20, eventCount * 3);
probability = Math.max(5, Math.min(90, probability));
// 时间范围:基于斜率估算何时触及阈值
let timeframe = '未来24小时';
if (slope > 0.01 && threshold > current) {
const hours = (threshold - current) / slope;
if (hours < 4) timeframe = '未来4小时';
else if (hours < 12) timeframe = '未来12小时';
else if (hours < 24) timeframe = '未来24小时';
else timeframe = `未来${Math.ceil(hours / 24)}天`;
}
anomalyPredictions.push({
metric: rule.name,
predictedAnomaly: rule.description || `${rule.name}可能触发`,
probability,
timeframe,
suggestedAction: anomalyAction(matchedDef.key),
});
} else {
// 非 metric 规则(如服务不可用、Pod重启)基于事件频率
const eventCount = alertEvents.filter(
(a) => a.rule_id === rule.id || a.rule_name === rule.name,
).length;
if (eventCount > 0) {
const exprKey = exprLower.includes('service') ? 'service' : exprLower.includes('pod') ? 'pod' : 'service';
anomalyPredictions.push({
metric: rule.name,
predictedAnomaly: rule.description || `${rule.name}可能再次发生`,
probability: Math.min(60, 20 + eventCount * 5),
timeframe: '未来24小时',
suggestedAction: anomalyAction(exprKey),
});
}
}
}
// 去重:同一指标名只保留概率最高的一条
const dedupedAnomalies = anomalyPredictions
.sort((a, b) => b.probability - a.probability)
.filter((item, idx, arr) => arr.findIndex((x) => x.metric === item.metric) === idx);
// ============ 概览卡片数据 ============
const avgR2 = METRIC_DEFS.length > 0
? METRIC_DEFS.reduce((sum, def) => sum + linearRegression(extractSeries(rawMetrics, def.key)).r2, 0) /
METRIC_DEFS.length
: 0;
const accuracy = Math.round(78 + avgR2 * 17);
const predictedAlertCount = dedupedAnomalies.filter((a) => a.probability >= 50).length;
const capacityWarningCount = capacityForecasts.filter((f) => f.daysUntilThreshold <= 14).length;
// ============ 综合预测推算(线性回归 + 告警规则阈值 + 事件频率) ============
const {
predictions,
capacityForecasts,
dedupedAnomalies,
accuracy,
predictedAlertCount,
capacityWarningCount,
} = computePredictions({ rawMetrics, serverStats, alertEvents, alertRules, selectedTimeRange });
const getTrendIcon = (trend: string) => {
switch (trend) {
@@ -346,17 +104,6 @@ export function AIPredictiveAnalytics() {
}
};
const getTrendColor = (trend: string) => {
switch (trend) {
case 'up':
return 'text-red-400';
case 'down':
return 'text-emerald-400';
default:
return 'text-slate-400';
}
};
return (
<div className="space-y-6">
{/* Header */}
@@ -511,7 +258,7 @@ export function AIPredictiveAnalytics() {
</div>
<div className="flex items-center gap-1">
{getTrendIcon(prediction.trend)}
<span className={`text-sm ${getTrendColor(prediction.trend)}`}>
<span className={`text-sm ${trendColorClass(prediction.trend)}`}>
{prediction.trend === 'up' ? '上升' : prediction.trend === 'down' ? '下降' : '平稳'}
</span>
</div>
@@ -527,7 +274,7 @@ export function AIPredictiveAnalytics() {
</div>
<div>
<p className="text-xs text-slate-500 mb-1">预测值 ({prediction.timeRange})</p>
<p className={`text-lg font-bold ${getTrendColor(prediction.trend)}`}>
<p className={`text-lg font-bold ${trendColorClass(prediction.trend)}`}>
{prediction.predicted}
{prediction.unit}
</p>
@@ -1,7 +1,7 @@
import { HardDrive, Database, Zap, Loader2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { CapacityForecast } from './AIPredictiveAnalytics';
import type { CapacityForecast } from './predictUtils';
export function PredictiveCapacityTab({
forecasts,
@@ -0,0 +1,320 @@
/**
* AI 预测分析模块的纯计算工具 — 类型定义、常量、线性回归、预测推算
*
* 从 AIPredictiveAnalytics.tsx(670行) 提取,避免主组件超 700 行硬上限。
* 所有函数均为纯函数(输入 → 输出,无副作用),便于单元测试和复用。
* // Logan.Li
*/
// ============ 类型定义 ============
export interface PredictionMetric {
name: string;
current: number;
predicted: number;
unit: string;
trend: 'up' | 'down' | 'stable';
confidence: number;
timeRange: string;
}
export interface CapacityForecast {
resource: string;
metricKey: string;
currentUsage: number;
predictedUsage: number;
threshold: number;
daysUntilThreshold: number;
recommendation: string;
}
export interface AnomalyPrediction {
metric: string;
predictedAnomaly: string;
probability: number;
timeframe: string;
suggestedAction: string;
}
// ============ 常量 ============
/** 指标定义:后端 metric name → 展示标签 / 告警表达式前缀 / 阈值 */
export const METRIC_DEFS = [
{ key: 'cpu', label: 'CPU使用率', exprKey: 'cpu_usage', threshold: 85 },
{ key: 'memory', label: '内存使用率', exprKey: 'memory_usage', threshold: 85 },
{ key: 'disk', label: '磁盘使用率', exprKey: 'disk_usage', threshold: 90 },
] as const;
export const TIME_RANGE_LABELS: Record<string, string> = {
'1h': '1小时后',
'24h': '24小时后',
'7d': '7天后',
'30d': '30天后',
};
export const TIME_RANGE_HOURS: Record<string, number> = {
'1h': 1,
'24h': 24,
'7d': 168,
'30d': 720,
};
// ============ 纯函数工具 ============
/** 简单最小二乘线性回归,返回斜率和 R² */
export function linearRegression(values: number[]): { slope: number; r2: number } {
const n = values.length;
if (n < 2) return { slope: 0, r2: 0 };
const xs = values.map((_, i) => i);
const meanX = xs.reduce((a, b) => a + b, 0) / n;
const meanY = values.reduce((a, b) => a + b, 0) / n;
let num = 0,
den = 0,
ssTot = 0,
ssRes = 0;
for (let i = 0; i < n; i++) {
num += (xs[i] - meanX) * (values[i] - meanY);
den += (xs[i] - meanX) ** 2;
ssTot += (values[i] - meanY) ** 2;
}
const slope = den === 0 ? 0 : num / den;
const intercept = meanY - slope * meanX;
for (let i = 0; i < n; i++) {
const pred = intercept + slope * xs[i];
ssRes += (values[i] - pred) ** 2;
}
const r2 = ssTot === 0 ? 1 : Math.max(0, 1 - ssRes / ssTot);
return { slope, r2 };
}
/** 从时序指标中提取按时间排序的数值序列 */
export function extractSeries(metrics: any[], name: string): number[] {
return metrics
.filter((m) => m.name === name)
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
.map((m) => Number(m.value) || 0);
}
/** 从告警表达式中解析阈值,如 "cpu_usage > 80" → 80 */
export function parseThreshold(expr: string): number | null {
const m = expr.match(/[><=]\s*([\d.]+)/);
return m ? parseFloat(m[1]) : null;
}
/** 容量规划建议 */
export function capacityRecommendation(metricKey: string, days: number): string {
const level = days <= 7 ? 'urgent' : days <= 14 ? 'warning' : 'normal';
const recs: Record<string, Record<string, string>> = {
urgent: {
cpu: 'CPU增长趋势明显,建议尽快扩容计算资源或优化高负载进程',
memory: '内存增长趋势明显,建议增加内存或排查内存泄漏',
disk: '磁盘空间即将耗尽,建议立即扩容或清理历史数据与日志',
},
warning: {
cpu: 'CPU使用率呈上升趋势,建议持续监控并准备扩容计划',
memory: '内存使用率呈上升趋势,建议优化应用内存使用',
disk: '磁盘使用率持续增长,建议规划数据归档和清理策略',
},
normal: {
cpu: 'CPU使用正常,建议持续监控',
memory: '内存使用正常,建议持续监控',
disk: '磁盘空间充足,建议持续监控',
},
};
return recs[level]?.[metricKey] || '建议持续监控资源使用趋势';
}
/** 异常建议操作 */
export function anomalyAction(metricKey: string): string {
const actions: Record<string, string> = {
cpu: '检查高负载进程,优化CPU密集型任务,考虑横向扩容',
memory: '排查内存泄漏,优化进程内存配置,考虑增加节点',
disk: '清理历史数据和日志,实施数据归档策略,扩容磁盘',
service: '检查服务健康状态与依赖组件,查看服务日志',
pod: '检查Pod重启原因,查看容器日志,排查OOM或健康检查失败',
};
return actions[metricKey] || '建议检查相关服务状态并持续监控';
}
// ============ 综合预测推算 ============
/** computePredictions 的输入参数 */
export interface ComputePredictionsInput {
rawMetrics: any[];
serverStats: any;
alertEvents: any[];
alertRules: any[];
selectedTimeRange: string; // '1h' | '24h' | '7d' | '30d'
}
/** computePredictions 的聚合输出 */
export interface ComputedPredictions {
predictions: PredictionMetric[];
capacityForecasts: CapacityForecast[];
dedupedAnomalies: AnomalyPrediction[];
accuracy: number;
predictedAlertCount: number;
capacityWarningCount: number;
}
/**
* 综合预测推算:基于时序指标 + 告警规则,计算指标预测、容量规划、异常预测。
* 主组件 AIPredictiveAnalytics.tsx 直接调用此函数获取所有展示数据。
* // Logan.Li
*/
export function computePredictions(input: ComputePredictionsInput): ComputedPredictions {
const { rawMetrics, serverStats, alertEvents, alertRules, selectedTimeRange } = input;
const horizonHours = TIME_RANGE_HOURS[selectedTimeRange] || 24;
const timeRangeLabel = TIME_RANGE_LABELS[selectedTimeRange] || '24小时后';
// ---- 指标预测 ----
const predictions: PredictionMetric[] = METRIC_DEFS.map((def) => {
const series = extractSeries(rawMetrics, def.key);
const { slope, r2 } = linearRegression(series);
const avgFallback =
def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk;
const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0;
let predicted = current + slope * horizonHours;
predicted = Math.max(0, Math.min(100, predicted));
const trend: 'up' | 'down' | 'stable' = slope > 0.05 ? 'up' : slope < -0.05 ? 'down' : 'stable';
const confidence = Math.round(60 + r2 * 35);
return {
name: def.label,
current: Math.round(current),
predicted: Math.round(predicted),
unit: '%',
trend,
confidence,
timeRange: timeRangeLabel,
};
});
// ---- 容量规划 ----
const capacityForecasts: CapacityForecast[] = METRIC_DEFS.map((def) => {
const series = extractSeries(rawMetrics, def.key);
const { slope } = linearRegression(series);
const avgFallback =
def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk;
const currentUsage = Math.round(series.length > 0 ? series[series.length - 1] : avgFallback || 0);
const slopePerDay = slope * 24;
const predictedUsage = Math.round(Math.min(100, currentUsage + slopePerDay * 7));
const threshold = def.threshold;
let daysUntilThreshold = 999;
if (slopePerDay > 0.01) {
daysUntilThreshold = Math.round((threshold - currentUsage) / slopePerDay);
daysUntilThreshold = daysUntilThreshold < 1 ? 1 : daysUntilThreshold;
}
const resourceNames: Record<string, string> = {
cpu: '服务器CPU算力',
memory: '服务器内存',
disk: '存储磁盘空间',
};
return {
resource: resourceNames[def.key] || def.label,
metricKey: def.key,
currentUsage,
predictedUsage,
threshold,
daysUntilThreshold,
recommendation: capacityRecommendation(def.key, daysUntilThreshold),
};
}).filter((f) => f.currentUsage > 0);
// ---- 异常预测(基于告警规则 + 趋势 + 事件频率) ----
const anomalyPredictions: AnomalyPrediction[] = [];
for (const rule of alertRules) {
if (!rule.enabled) continue;
const threshold = parseThreshold(rule.expression || '');
const exprLower = (rule.expression || '').toLowerCase();
const matchedDef = METRIC_DEFS.find((def) => exprLower.includes(def.exprKey));
if (matchedDef && threshold) {
const series = extractSeries(rawMetrics, matchedDef.key);
const { slope } = linearRegression(series);
const avgFallback =
matchedDef.key === 'cpu'
? serverStats.avg_cpu
: matchedDef.key === 'memory'
? serverStats.avg_memory
: serverStats.avg_disk;
const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0;
const ratio = threshold > 0 ? current / threshold : 0;
let probability = Math.round(ratio * 55);
if (slope > 0.05) probability += 15;
const eventCount = alertEvents.filter(
(a) => a.rule_id === rule.id || a.rule_name === rule.name,
).length;
probability += Math.min(20, eventCount * 3);
probability = Math.max(5, Math.min(90, probability));
// 时间范围:基于斜率估算何时触及阈值
let timeframe = '未来24小时';
if (slope > 0.01 && threshold > current) {
const hours = (threshold - current) / slope;
if (hours < 4) timeframe = '未来4小时';
else if (hours < 12) timeframe = '未来12小时';
else if (hours < 24) timeframe = '未来24小时';
else timeframe = `未来${Math.ceil(hours / 24)}天`;
}
anomalyPredictions.push({
metric: rule.name,
predictedAnomaly: rule.description || `${rule.name}可能触发`,
probability,
timeframe,
suggestedAction: anomalyAction(matchedDef.key),
});
} else {
// 非 metric 规则(如服务不可用、Pod重启)基于事件频率
const eventCount = alertEvents.filter(
(a) => a.rule_id === rule.id || a.rule_name === rule.name,
).length;
if (eventCount > 0) {
const exprKey = exprLower.includes('service') ? 'service' : exprLower.includes('pod') ? 'pod' : 'service';
anomalyPredictions.push({
metric: rule.name,
predictedAnomaly: rule.description || `${rule.name}可能再次发生`,
probability: Math.min(60, 20 + eventCount * 5),
timeframe: '未来24小时',
suggestedAction: anomalyAction(exprKey),
});
}
}
}
// 去重:同一指标名只保留概率最高的一条
const dedupedAnomalies = anomalyPredictions
.sort((a, b) => b.probability - a.probability)
.filter((item, idx, arr) => arr.findIndex((x) => x.metric === item.metric) === idx);
// ---- 概览统计 ----
const avgR2 = METRIC_DEFS.length > 0
? METRIC_DEFS.reduce((sum, def) => sum + linearRegression(extractSeries(rawMetrics, def.key)).r2, 0) /
METRIC_DEFS.length
: 0;
const accuracy = Math.round(78 + avgR2 * 17);
const predictedAlertCount = dedupedAnomalies.filter((a) => a.probability >= 50).length;
const capacityWarningCount = capacityForecasts.filter((f) => f.daysUntilThreshold <= 14).length;
return {
predictions,
capacityForecasts,
dedupedAnomalies,
accuracy,
predictedAlertCount,
capacityWarningCount,
};
}
/** 趋势图标颜色工具,供主组件 render 用 */
export function trendColorClass(trend: string): string {
switch (trend) {
case 'up':
return 'text-red-400';
case 'down':
return 'text-emerald-400';
default:
return 'text-slate-400';
}
}