refactor: 拆分AIPredictiveAnalytics.tsx(670→417行)提取predictUtils子模块
This commit is contained in:
1 parent
67066041b3
commit
11c97cd885
3 files changed
+339
-272
No files matched your search
@@ -20,143 +20,25 @@ import {
|
|||||||
Cpu,
|
Cpu,
|
||||||
MemoryStick,
|
MemoryStick,
|
||||||
Network,
|
Network,
|
||||||
Database,
|
|
||||||
Activity,
|
Activity,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
LineChart,
|
LineChart,
|
||||||
Calendar,
|
Calendar,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Zap,
|
Zap,
|
||||||
ArrowUpRight,
|
|
||||||
Minus,
|
Minus,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { dashboardStatsApi, serverApi, alertApi } from '@/services/api';
|
import { dashboardStatsApi, serverApi, alertApi } from '@/services/api';
|
||||||
import { PredictiveCapacityTab } from './PredictiveCapacityTab';
|
import { PredictiveCapacityTab } from './PredictiveCapacityTab';
|
||||||
|
import {
|
||||||
|
computePredictions,
|
||||||
|
trendColorClass,
|
||||||
|
type CapacityForecast,
|
||||||
|
} from './predictUtils';
|
||||||
|
|
||||||
interface PredictionMetric {
|
// 重新导出 CapacityForecast,兼容已有 import 路径 './AIPredictiveAnalytics'
|
||||||
name: string;
|
export type { CapacityForecast };
|
||||||
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] || '建议检查相关服务状态并持续监控';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AIPredictiveAnalytics() {
|
export function AIPredictiveAnalytics() {
|
||||||
const [selectedTimeRange, setSelectedTimeRange] = useState('24h');
|
const [selectedTimeRange, setSelectedTimeRange] = useState('24h');
|
||||||
@@ -201,139 +83,15 @@ export function AIPredictiveAnalytics() {
|
|||||||
setIsAnalyzing(false);
|
setIsAnalyzing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============ 计算指标预测 ============
|
// ============ 综合预测推算(线性回归 + 告警规则阈值 + 事件频率) ============
|
||||||
const horizonHours = TIME_RANGE_HOURS[selectedTimeRange] || 24;
|
const {
|
||||||
const timeRangeLabel = TIME_RANGE_LABELS[selectedTimeRange] || '24小时后';
|
predictions,
|
||||||
|
capacityForecasts,
|
||||||
const predictions: PredictionMetric[] = METRIC_DEFS.map((def) => {
|
dedupedAnomalies,
|
||||||
const series = extractSeries(rawMetrics, def.key);
|
accuracy,
|
||||||
const { slope, r2 } = linearRegression(series);
|
predictedAlertCount,
|
||||||
const avgFallback =
|
capacityWarningCount,
|
||||||
def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk;
|
} = computePredictions({ rawMetrics, serverStats, alertEvents, alertRules, selectedTimeRange });
|
||||||
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 getTrendIcon = (trend: string) => {
|
const getTrendIcon = (trend: string) => {
|
||||||
switch (trend) {
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -511,7 +258,7 @@ export function AIPredictiveAnalytics() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{getTrendIcon(prediction.trend)}
|
{getTrendIcon(prediction.trend)}
|
||||||
<span className={`text-sm ${getTrendColor(prediction.trend)}`}>
|
<span className={`text-sm ${trendColorClass(prediction.trend)}`}>
|
||||||
{prediction.trend === 'up' ? '上升' : prediction.trend === 'down' ? '下降' : '平稳'}
|
{prediction.trend === 'up' ? '上升' : prediction.trend === 'down' ? '下降' : '平稳'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -527,7 +274,7 @@ export function AIPredictiveAnalytics() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 mb-1">预测值 ({prediction.timeRange})</p>
|
<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.predicted}
|
||||||
{prediction.unit}
|
{prediction.unit}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HardDrive, Database, Zap, Loader2 } from 'lucide-react';
|
import { HardDrive, Database, Zap, Loader2 } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import type { CapacityForecast } from './AIPredictiveAnalytics';
|
import type { CapacityForecast } from './predictUtils';
|
||||||
|
|
||||||
export function PredictiveCapacityTab({
|
export function PredictiveCapacityTab({
|
||||||
forecasts,
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user