155 lines
4.7 KiB
TypeScript
155 lines
4.7 KiB
TypeScript
import { query } from '../../utils/db'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const SLOPE_THRESHOLD = 0.5
|
|
const MIN_SAMPLES = 10
|
|
const WINDOW_MINUTES = 30
|
|
|
|
const servers = await query<any>(`
|
|
SELECT target_id, server_name
|
|
FROM server_targets
|
|
WHERE is_active = 1
|
|
ORDER BY server_name
|
|
`)
|
|
|
|
const anomalies: any[] = []
|
|
const serverResults: any[] = []
|
|
|
|
for (const server of servers) {
|
|
const snapshots = await query<any>(`
|
|
SELECT cpu_percent, memory_percent, collected_at,
|
|
EXTRACT(EPOCH FROM (NOW() - collected_at::timestamp)) / 60 as minutes_ago
|
|
FROM server_snapshots
|
|
WHERE target_id = $1 AND is_online = 1
|
|
AND collected_at::timestamp >= NOW() - INTERVAL '${WINDOW_MINUTES} minutes'
|
|
ORDER BY collected_at ASC
|
|
`, [server.target_id])
|
|
|
|
if (snapshots.length < MIN_SAMPLES) {
|
|
serverResults.push({
|
|
target_id: server.target_id,
|
|
server_name: server.server_name,
|
|
cpu_current: snapshots.length > 0 ? snapshots[snapshots.length - 1].cpu_percent : null,
|
|
mem_current: snapshots.length > 0 ? snapshots[snapshots.length - 1].memory_percent : null,
|
|
cpu_slope: null,
|
|
mem_slope: null,
|
|
cpu_trend: null,
|
|
mem_trend: null,
|
|
sample_count: snapshots.length,
|
|
status: 'insufficient'
|
|
})
|
|
continue
|
|
}
|
|
|
|
const n = snapshots.length
|
|
const current = snapshots[n - 1]
|
|
const currCpu = Number(current.cpu_percent) || 0
|
|
const currMem = Number(current.memory_percent) || 0
|
|
|
|
const cpuPoints = snapshots.map((s: any, i: number) => ({ x: i, y: Number(s.cpu_percent) || 0 }))
|
|
const memPoints = snapshots.map((s: any, i: number) => ({ x: i, y: Number(s.memory_percent) || 0 }))
|
|
|
|
function linearRegression(points: { x: number, y: number }[]): { slope: number, intercept: number, r2: number } {
|
|
const n = points.length
|
|
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0
|
|
|
|
for (const p of points) {
|
|
sumX += p.x
|
|
sumY += p.y
|
|
sumXY += p.x * p.y
|
|
sumX2 += p.x * p.x
|
|
}
|
|
|
|
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX)
|
|
const intercept = (sumY - slope * sumX) / n
|
|
|
|
const yMean = sumY / n
|
|
let ssTotal = 0, ssResidual = 0
|
|
for (const p of points) {
|
|
const yPred = slope * p.x + intercept
|
|
ssTotal += Math.pow(p.y - yMean, 2)
|
|
ssResidual += Math.pow(p.y - yPred, 2)
|
|
}
|
|
const r2 = ssTotal > 0 ? 1 - (ssResidual / ssTotal) : 0
|
|
|
|
return { slope, intercept, r2 }
|
|
}
|
|
|
|
const cpuReg = linearRegression(cpuPoints)
|
|
const memReg = linearRegression(memPoints)
|
|
|
|
const cpuSlopePerMin = (cpuReg.slope * n) / WINDOW_MINUTES
|
|
const memSlopePerMin = (memReg.slope * n) / WINDOW_MINUTES
|
|
|
|
function getTrend(slope: number, r2: number): string {
|
|
if (r2 < 0.3) return 'unstable'
|
|
if (slope >= SLOPE_THRESHOLD) return 'rising'
|
|
if (slope <= -SLOPE_THRESHOLD) return 'falling'
|
|
return 'stable'
|
|
}
|
|
|
|
const cpuTrend = getTrend(cpuSlopePerMin, cpuReg.r2)
|
|
const memTrend = getTrend(memSlopePerMin, memReg.r2)
|
|
|
|
let status = 'normal'
|
|
if (cpuTrend === 'rising' || memTrend === 'rising') status = 'warning'
|
|
if (cpuSlopePerMin >= 1.0 || memSlopePerMin >= 1.0) status = 'danger'
|
|
|
|
serverResults.push({
|
|
target_id: server.target_id,
|
|
server_name: server.server_name,
|
|
cpu_current: currCpu,
|
|
mem_current: currMem,
|
|
cpu_slope: cpuSlopePerMin,
|
|
mem_slope: memSlopePerMin,
|
|
cpu_trend: cpuTrend,
|
|
mem_trend: memTrend,
|
|
cpu_r2: cpuReg.r2,
|
|
mem_r2: memReg.r2,
|
|
sample_count: snapshots.length,
|
|
status
|
|
})
|
|
|
|
if (cpuTrend === 'rising' && cpuReg.r2 >= 0.3) {
|
|
const level = cpuSlopePerMin >= 1.0 ? 'danger' : 'warning'
|
|
anomalies.push({
|
|
target_id: server.target_id,
|
|
server_name: server.server_name,
|
|
metric: 'CPU',
|
|
current: currCpu,
|
|
slope: cpuSlopePerMin,
|
|
r2: cpuReg.r2,
|
|
trend: cpuTrend,
|
|
level
|
|
})
|
|
}
|
|
|
|
if (memTrend === 'rising' && memReg.r2 >= 0.3) {
|
|
const level = memSlopePerMin >= 1.0 ? 'danger' : 'warning'
|
|
anomalies.push({
|
|
target_id: server.target_id,
|
|
server_name: server.server_name,
|
|
metric: 'Memory',
|
|
current: currMem,
|
|
slope: memSlopePerMin,
|
|
r2: memReg.r2,
|
|
trend: memTrend,
|
|
level
|
|
})
|
|
}
|
|
}
|
|
|
|
anomalies.sort((a, b) => b.slope - a.slope)
|
|
|
|
return {
|
|
anomalies,
|
|
servers: serverResults,
|
|
config: {
|
|
slope_threshold: SLOPE_THRESHOLD,
|
|
window_minutes: WINDOW_MINUTES,
|
|
min_samples: MIN_SAMPLES
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
})
|