소스 수정
This commit is contained in:
@@ -6,54 +6,44 @@ export default defineEventHandler(async (event) => {
|
||||
const type = queryParams.type as string || 'short-term'
|
||||
const period = queryParams.period as string || '24h'
|
||||
|
||||
// 기간/간격 계산
|
||||
let interval = '24 hours'
|
||||
let groupFormat = 'YYYY-MM-DD HH24:00'
|
||||
// 기간/간격 설정
|
||||
const config = getPeriodConfig(period)
|
||||
|
||||
if (period === '1h') {
|
||||
interval = '1 hour'
|
||||
groupFormat = 'YYYY-MM-DD HH24:MI'
|
||||
} else if (period === '6h') {
|
||||
interval = '6 hours'
|
||||
groupFormat = 'YYYY-MM-DD HH24:00'
|
||||
} else if (period === '12h') {
|
||||
interval = '12 hours'
|
||||
groupFormat = 'YYYY-MM-DD HH24:00'
|
||||
} else if (period === '24h') {
|
||||
interval = '24 hours'
|
||||
groupFormat = 'YYYY-MM-DD HH24:00'
|
||||
} else if (period === '7d') {
|
||||
interval = '7 days'
|
||||
groupFormat = 'YYYY-MM-DD'
|
||||
} else if (period === '30d') {
|
||||
interval = '30 days'
|
||||
groupFormat = 'YYYY-MM-DD'
|
||||
}
|
||||
// 시간 슬롯 생성 (연속된 X축)
|
||||
const timeSlots = generateTimeSlots(config)
|
||||
|
||||
// 시간대별 집계 (anomaly_logs 테이블이 없으면 빈 배열 반환)
|
||||
// DB에서 로그 조회
|
||||
let rows: any[] = []
|
||||
try {
|
||||
rows = await query<any>(`
|
||||
SELECT
|
||||
to_char(detected_at, '${groupFormat}') as time_slot,
|
||||
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 >= NOW() - INTERVAL '${interval}'
|
||||
AND detected_at::timestamp >= NOW() - INTERVAL '${config.interval}'
|
||||
GROUP BY time_slot
|
||||
ORDER BY time_slot ASC
|
||||
`, [type])
|
||||
} catch (e) {
|
||||
// 테이블이 없으면 빈 배열
|
||||
rows = []
|
||||
}
|
||||
|
||||
// 시간 포맷 변환
|
||||
const data = rows.map(r => ({
|
||||
time: formatTimeLabel(r.time_slot, period),
|
||||
warning: Number(r.warning),
|
||||
danger: Number(r.danger)
|
||||
// 로그 데이터를 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 {
|
||||
@@ -64,17 +54,134 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
})
|
||||
|
||||
function formatTimeLabel(timeSlot: string, period: string): string {
|
||||
if (!timeSlot) return ''
|
||||
|
||||
if (period === '7d' || period === '30d') {
|
||||
const parts = timeSlot.split('-')
|
||||
return `${parts[1]}/${parts[2]}`
|
||||
} else {
|
||||
const parts = timeSlot.split(' ')
|
||||
if (parts.length === 2) {
|
||||
return parts[1].substring(0, 5)
|
||||
}
|
||||
return timeSlot.substring(11, 16)
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -288,8 +288,24 @@ async function getServerDashboard() {
|
||||
})
|
||||
}
|
||||
|
||||
// 서버 정렬: 이름 순
|
||||
serverStatuses.sort((a, b) => a.server_name.localeCompare(b.server_name))
|
||||
// 서버 정렬: 장애 우선 → 컨테이너 많은 순 → 이름순
|
||||
serverStatuses.sort((a, b) => {
|
||||
// 1. 장애 여부 (서버 장애 또는 컨테이너에 장애)
|
||||
const aHasIssue = a.level !== 'normal' ||
|
||||
(a.container_summary.stopped > 0 || a.container_summary.critical > 0 || a.container_summary.warning > 0)
|
||||
const bHasIssue = b.level !== 'normal' ||
|
||||
(b.container_summary.stopped > 0 || b.container_summary.critical > 0 || b.container_summary.warning > 0)
|
||||
|
||||
if (aHasIssue !== bHasIssue) return aHasIssue ? -1 : 1
|
||||
|
||||
// 2. 컨테이너 수 (많은 순)
|
||||
const aContainers = a.container_summary.total || 0
|
||||
const bContainers = b.container_summary.total || 0
|
||||
if (aContainers !== bContainers) return bContainers - aContainers
|
||||
|
||||
// 3. 서버 이름순
|
||||
return a.server_name.localeCompare(b.server_name)
|
||||
})
|
||||
|
||||
return {
|
||||
summary: { servers: summaryServers, containers: summaryContainers },
|
||||
|
||||
Reference in New Issue
Block a user