Files
system-monitor/backend/api/anomaly/chart.get.ts
2025-12-28 16:35:14 +09:00

188 lines
5.1 KiB
TypeScript

import { query } from '../../utils/db'
export default defineEventHandler(async (event) => {
const queryParams = getQuery(event)
const type = queryParams.type as string || 'short-term'
const period = queryParams.period as string || '24h'
// 기간/간격 설정
const config = getPeriodConfig(period)
// 시간 슬롯 생성 (연속된 X축)
const timeSlots = generateTimeSlots(config)
// DB에서 로그 조회
let rows: any[] = []
try {
rows = await query<any>(`
SELECT
to_char(detected_at::timestamp, '${config.dbFormat}') as time_slot,
SUM(CASE WHEN level = 'warning' THEN 1 ELSE 0 END) as warning,
SUM(CASE WHEN level = 'danger' THEN 1 ELSE 0 END) as danger
FROM anomaly_logs
WHERE detect_type = $1
AND detected_at::timestamp >= NOW() - INTERVAL '${config.interval}'
GROUP BY time_slot
ORDER BY time_slot ASC
`, [type])
} catch (e) {
rows = []
}
// 로그 데이터를 Map으로 변환
const logMap = new Map<string, { warning: number; danger: number }>()
for (const r of rows) {
logMap.set(r.time_slot, {
warning: Number(r.warning) || 0,
danger: Number(r.danger) || 0
})
}
// 전체 시간 슬롯에 데이터 매핑 (없으면 0)
const data = timeSlots.map(slot => ({
time: slot.label,
warning: logMap.get(slot.key)?.warning || 0,
danger: logMap.get(slot.key)?.danger || 0
}))
return {
data,
type,
period,
timestamp: new Date().toISOString()
}
})
interface PeriodConfig {
interval: string
stepMinutes: number
dbFormat: string
labelFormat: (date: Date) => string
}
function getPeriodConfig(period: string): PeriodConfig {
switch (period) {
case '1h':
return {
interval: '1 hour',
stepMinutes: 5,
dbFormat: 'YYYY-MM-DD HH24:MI',
labelFormat: (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`
}
case '6h':
return {
interval: '6 hours',
stepMinutes: 30,
dbFormat: 'YYYY-MM-DD HH24:MI',
labelFormat: (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`
}
case '12h':
return {
interval: '12 hours',
stepMinutes: 60,
dbFormat: 'YYYY-MM-DD HH24:00',
labelFormat: (d) => `${pad(d.getHours())}:00`
}
case '24h':
return {
interval: '24 hours',
stepMinutes: 60,
dbFormat: 'YYYY-MM-DD HH24:00',
labelFormat: (d) => `${pad(d.getHours())}:00`
}
case '7d':
return {
interval: '7 days',
stepMinutes: 360, // 6시간
dbFormat: 'YYYY-MM-DD HH24:00',
labelFormat: (d) => `${pad(d.getMonth()+1)}/${pad(d.getDate())} ${pad(d.getHours())}`
}
case '30d':
return {
interval: '30 days',
stepMinutes: 1440, // 1일
dbFormat: 'YYYY-MM-DD',
labelFormat: (d) => `${pad(d.getMonth()+1)}/${pad(d.getDate())}`
}
default:
return {
interval: '24 hours',
stepMinutes: 60,
dbFormat: 'YYYY-MM-DD HH24:00',
labelFormat: (d) => `${pad(d.getHours())}:00`
}
}
}
function generateTimeSlots(config: PeriodConfig): { key: string; label: string }[] {
const slots: { key: string; label: string }[] = []
const now = new Date()
// 현재 시간을 간격에 맞게 내림
const roundedNow = new Date(now)
if (config.stepMinutes >= 1440) {
// 일 단위: 오늘 00:00
roundedNow.setHours(0, 0, 0, 0)
} else if (config.stepMinutes >= 60) {
// 시간 단위: 현재 시간 00분
roundedNow.setMinutes(0, 0, 0)
} else {
// 분 단위: 가장 가까운 간격
const mins = roundedNow.getMinutes()
const rounded = Math.floor(mins / config.stepMinutes) * config.stepMinutes
roundedNow.setMinutes(rounded, 0, 0)
}
// interval을 밀리초로 변환
const intervalMs = parseInterval(config.interval)
const startTime = new Date(roundedNow.getTime() - intervalMs)
// 시작부터 현재까지 슬롯 생성
const stepMs = config.stepMinutes * 60 * 1000
let current = new Date(startTime)
while (current <= roundedNow) {
const key = formatDbKey(current, config.dbFormat)
const label = config.labelFormat(current)
slots.push({ key, label })
current = new Date(current.getTime() + stepMs)
}
return slots
}
function parseInterval(interval: string): number {
const match = interval.match(/(\d+)\s*(hour|hours|day|days)/)
if (!match) return 24 * 60 * 60 * 1000
const num = parseInt(match[1])
const unit = match[2]
if (unit.startsWith('day')) {
return num * 24 * 60 * 60 * 1000
} else {
return num * 60 * 60 * 1000
}
}
function formatDbKey(date: Date, format: string): string {
const y = date.getFullYear()
const m = pad(date.getMonth() + 1)
const d = pad(date.getDate())
const h = pad(date.getHours())
const mi = pad(date.getMinutes())
if (format === 'YYYY-MM-DD') {
return `${y}-${m}-${d}`
} else if (format === 'YYYY-MM-DD HH24:00') {
return `${y}-${m}-${d} ${h}:00`
} else {
return `${y}-${m}-${d} ${h}:${mi}`
}
}
function pad(n: number): string {
return n.toString().padStart(2, '0')
}