2178 lines
94 KiB
Python
2178 lines
94 KiB
Python
"""
|
||
워커 감시 대시보드
|
||
실시간으로 워커 상태, GPU 사용량, 세션 풀 상태를 모니터링합니다.
|
||
Jetson Xavier와 x86 시스템을 모두 지원합니다.
|
||
"""
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
import time
|
||
import psutil
|
||
import os
|
||
from datetime import datetime, timedelta
|
||
from typing import Dict, List, Any
|
||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.responses import HTMLResponse
|
||
import uvicorn
|
||
from fastapi import APIRouter, Request
|
||
import websockets.exceptions
|
||
import requests
|
||
import subprocess
|
||
from ..utils.discord_notifier import send_discord_notification
|
||
|
||
from ..core.worker_manager import worker_manager
|
||
from ..core.session_pool import session_pool
|
||
from ..utils.gpu_monitor import gpu_monitor
|
||
from ..core.config import settings
|
||
|
||
# main_app = None
|
||
|
||
# def init_monitoring(app: FastAPI):
|
||
# """모니터링 앱을 초기화하고 메인 앱 객체를 설정합니다."""
|
||
# global main_app
|
||
# main_app = app
|
||
# # lifespan에서 worker_manager와 session_pool이 app.state에 설정되도록 합니다.
|
||
# @app.on_event("startup")
|
||
# async def startup_event():
|
||
# if not hasattr(app.state, 'worker_manager') or not hasattr(app.state, 'session_pool'):
|
||
# # main.py의 lifespan에서 설정되므로, 여기서는 경고만 로깅
|
||
# logger.warning("worker_manager 또는 session_pool이 app.state에 설정되지 않았습니다.")
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# main.py에서 공유할 객체들 -> 이제 Request 객체를 통해 접근합니다.
|
||
# worker_manager = None
|
||
# session_pool = None
|
||
|
||
# def set_shared_objects(wm, sp):
|
||
# """메인 서버의 worker_manager와 session_pool을 설정합니다."""
|
||
# global worker_manager, session_pool
|
||
# worker_manager = wm
|
||
# session_pool = sp
|
||
|
||
def read_status_from_file():
|
||
"""status.json 파일에서 상태를 읽어옵니다."""
|
||
try:
|
||
with open("status.json", "r") as f:
|
||
return json.load(f)
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
return {
|
||
"worker_status": {"running": False, "total_workers": 0, "queue_size": 0, "workers_by_status": {}},
|
||
"session_status": {},
|
||
"timestamp": 0
|
||
}
|
||
|
||
# API 라우터 생성
|
||
api_router = APIRouter()
|
||
|
||
# 모니터링 앱 생성
|
||
monitor_app = FastAPI(
|
||
title="인페인팅 서버 모니터링 대시보드",
|
||
description="실시간 서버 상태 모니터링 (Jetson Xavier & x86 지원)",
|
||
version="1.0.0"
|
||
)
|
||
|
||
# 연결된 WebSocket 클라이언트들
|
||
connected_clients: List[WebSocket] = []
|
||
|
||
|
||
class MonitoringData:
|
||
def __init__(self):
|
||
self.history: List[Dict[str, Any]] = []
|
||
self.max_history = 100 # 최대 100개 데이터 포인트 저장
|
||
self.api_stats = {
|
||
"total_requests": 0,
|
||
"successful_requests": 0,
|
||
"failed_requests": 0,
|
||
"endpoint_usage": {},
|
||
"response_times": [],
|
||
"errors": []
|
||
}
|
||
self.alerts = []
|
||
|
||
async def collect_data(self) -> Dict[str, Any]:
|
||
"""주기적으로 서버 상태 데이터를 수집합니다."""
|
||
try:
|
||
logger.info("데이터 수집 시작")
|
||
|
||
# status.json 파일에서 상태 읽기
|
||
status = read_status_from_file()
|
||
logger.info(f"status.json 읽기 완료: {bool(status)}")
|
||
|
||
worker_status = status.get("worker_status", {})
|
||
session_status = status.get("session_status", {})
|
||
api_stats = status.get("api_stats", {})
|
||
timestamp = status.get("timestamp", 0)
|
||
|
||
logger.info(f"워커 상태: {bool(worker_status)}, 세션 상태: {bool(session_status)}")
|
||
|
||
# status.json에서 읽어온 데이터가 없으면 기본값 사용
|
||
if not worker_status:
|
||
logger.info("워커 상태가 비어있어 기본값 사용")
|
||
worker_status = self._get_default_worker_status()
|
||
|
||
# 항상 실제 세션 풀 상태 사용 (동적 데이터)
|
||
try:
|
||
real_session_status = session_pool.get_status()
|
||
session_status = real_session_status
|
||
logger.debug(f"실시간 세션 풀 상태 사용: {real_session_status}")
|
||
|
||
if not session_status:
|
||
logger.info("세션 상태가 비어있어 기본값 사용")
|
||
session_status = self._get_default_session_status()
|
||
except Exception as e:
|
||
logger.warning(f"실제 세션 풀 상태 조회 실패: {e}")
|
||
session_status = status.get("session_status", {})
|
||
if not session_status:
|
||
logger.info("status.json 세션 상태도 비어있어 기본값 사용")
|
||
session_status = self._get_default_session_status()
|
||
|
||
# GPU 정보 (안전하게 가져오기)
|
||
gpu_info = {}
|
||
try:
|
||
logger.info("GPU 정보 수집 시작")
|
||
gpu_info = {
|
||
**gpu_monitor.get_gpu_memory_info(),
|
||
"utilization": gpu_monitor.get_gpu_utilization()
|
||
}
|
||
|
||
# Jetson의 경우 추가 GPU 정보
|
||
if settings.IS_JETSON:
|
||
jetson_gpu_info = gpu_monitor.get_jetson_specific_info()
|
||
if jetson_gpu_info:
|
||
# GPU 온도 정보
|
||
if jetson_gpu_info.get("temperature"):
|
||
temps = jetson_gpu_info["temperature"]
|
||
# 가장 높은 온도를 GPU 온도로 사용
|
||
if temps:
|
||
gpu_temp = max(temps.values()) if temps else 0
|
||
gpu_info["temperature"] = gpu_temp
|
||
else:
|
||
gpu_info["temperature"] = "미지원"
|
||
else:
|
||
gpu_info["temperature"] = "미지원"
|
||
|
||
# GPU 클럭 정보
|
||
gpu_freq = jetson_gpu_info.get("gpu_frequency")
|
||
if gpu_freq is not None:
|
||
gpu_info["clock_speed"] = gpu_freq
|
||
else:
|
||
gpu_info["clock_speed"] = "미지원"
|
||
else:
|
||
# x86 시스템의 경우 NVML을 통해 온도/클럭 정보 시도
|
||
try:
|
||
# 여기에 x86 GPU 정보 추가 로직을 구현할 수 있음
|
||
gpu_info["temperature"] = "미지원"
|
||
gpu_info["clock_speed"] = "미지원"
|
||
except:
|
||
gpu_info["temperature"] = "미지원"
|
||
gpu_info["clock_speed"] = "미지원"
|
||
|
||
logger.info("GPU 정보 수집 완료")
|
||
except Exception as e:
|
||
logger.warning(f"GPU 정보 조회 실패: {e}")
|
||
gpu_info = {"total": 0, "used": 0, "free": 0, "usage_percent": 0, "utilization": 0, "temperature": "오류", "clock_speed": "오류"}
|
||
|
||
# 시스템 메모리 정보 (안전하게 가져오기)
|
||
system_memory = {}
|
||
try:
|
||
logger.info("시스템 메모리 정보 수집 시작")
|
||
system_memory = gpu_monitor.get_system_memory_info()
|
||
logger.info("시스템 메모리 정보 수집 완료")
|
||
except Exception as e:
|
||
logger.warning(f"시스템 메모리 정보 조회 실패: {e}")
|
||
system_memory = {"total": 0, "used": 0, "free": 0, "usage_percent": 0}
|
||
|
||
# 시스템 성능 정보 (안전하게 가져오기)
|
||
system_performance = {}
|
||
try:
|
||
logger.info("시스템 성능 정보 수집 시작")
|
||
system_performance = self._get_system_performance()
|
||
logger.info("시스템 성능 정보 수집 완료")
|
||
except Exception as e:
|
||
logger.warning(f"시스템 성능 정보 조회 실패: {e}")
|
||
system_performance = {"cpu_percent": 0, "cpu_count": 1, "cpu_freq": 0}
|
||
|
||
# Jetson 전용 정보 (안전하게 가져오기)
|
||
jetson_info = {}
|
||
if settings.IS_JETSON:
|
||
try:
|
||
logger.info("Jetson 전용 정보 수집 시작")
|
||
jetson_info = gpu_monitor.get_jetson_specific_info()
|
||
if jetson_info is None:
|
||
jetson_info = {}
|
||
logger.info("Jetson 전용 정보 수집 완료")
|
||
except Exception as e:
|
||
logger.warning(f"Jetson 전용 정보 조회 실패: {e}")
|
||
jetson_info = {}
|
||
|
||
# API 통계는 status.json에서 읽어온 것을 사용
|
||
if not api_stats:
|
||
logger.info("API 통계가 비어있어 기본값 사용")
|
||
api_stats = self._get_api_statistics()
|
||
|
||
# 모델별 성능 통계 직접 조회
|
||
model_performance_stats = {}
|
||
try:
|
||
logger.info("모델 성능 통계 조회 시작")
|
||
# 메인 서버의 stats 엔드포인트 호출
|
||
response = requests.get(f"http://{settings.HOST}:{settings.PORT}/api/v1/stats", timeout=2)
|
||
if response.status_code == 200:
|
||
model_performance_stats = response.json()
|
||
logger.info("모델 성능 통계 조회 완료")
|
||
else:
|
||
logger.warning(f"모델 성능 통계 조회 실패: 상태 코드 {response.status_code}")
|
||
except requests.RequestException as e:
|
||
logger.error(f"모델 성능 통계 조회 중 예외 발생: {e}")
|
||
|
||
# 알림 및 경고 (안전하게 가져오기)
|
||
alerts = []
|
||
try:
|
||
logger.info("알림 확인 시작")
|
||
alerts = self._check_alerts(worker_status)
|
||
logger.info("알림 확인 완료")
|
||
except Exception as e:
|
||
logger.warning(f"알림 확인 실패: {e}")
|
||
alerts = []
|
||
|
||
logger.info("데이터 구조 생성 시작")
|
||
data = {
|
||
"timestamp": datetime.now().isoformat(),
|
||
"system_type": "Jetson Xavier" if settings.IS_JETSON else "x86_64",
|
||
"gpu": gpu_info,
|
||
"system_memory": system_memory,
|
||
"system_performance": system_performance,
|
||
"workers": worker_status,
|
||
"sessions": session_status,
|
||
"jetson": jetson_info,
|
||
"api_stats": api_stats,
|
||
"model_performance_stats": model_performance_stats,
|
||
"alerts": alerts
|
||
}
|
||
|
||
logger.info("히스토리에 데이터 추가 시작")
|
||
# 히스토리에 추가
|
||
self.history.append(data)
|
||
if len(self.history) > self.max_history:
|
||
self.history.pop(0)
|
||
|
||
logger.info("데이터 수집 완료")
|
||
return data
|
||
|
||
except Exception as e:
|
||
logger.error(f"데이터 수집 중 오류 발생: {e}")
|
||
import traceback
|
||
logger.error(f"상세 오류: {traceback.format_exc()}")
|
||
|
||
# 기본 데이터 반환
|
||
return {
|
||
"timestamp": datetime.now().isoformat(),
|
||
"system_type": "Jetson Xavier" if settings.IS_JETSON else "x86_64",
|
||
"gpu": {"total": 0, "used": 0, "free": 0, "usage_percent": 0, "utilization": 0},
|
||
"system_memory": {"total": 0, "used": 0, "free": 0, "usage_percent": 0},
|
||
"system_performance": {"cpu_percent": 0, "cpu_count": 1, "cpu_freq": 0},
|
||
"workers": self._get_default_worker_status(),
|
||
"sessions": self._get_default_session_status(),
|
||
"jetson": {},
|
||
"api_stats": self._get_api_statistics(),
|
||
"model_performance_stats": {},
|
||
"alerts": [],
|
||
"error": str(e)
|
||
}
|
||
|
||
def _get_system_performance(self) -> Dict[str, Any]:
|
||
"""시스템 성능 지표를 수집합니다."""
|
||
try:
|
||
# CPU 사용률
|
||
cpu_percent = psutil.cpu_percent(interval=1)
|
||
cpu_count = psutil.cpu_count()
|
||
cpu_freq = psutil.cpu_freq()
|
||
|
||
# 디스크 I/O
|
||
disk_io = psutil.disk_io_counters()
|
||
|
||
# 네트워크 I/O
|
||
net_io = psutil.net_io_counters()
|
||
|
||
# 프로세스 정보
|
||
processes = len(psutil.pids())
|
||
|
||
# 시스템 부하
|
||
load_avg = os.getloadavg() if hasattr(os, 'getloadavg') else [0, 0, 0]
|
||
|
||
return {
|
||
"cpu": {
|
||
"usage_percent": cpu_percent,
|
||
"count": cpu_count,
|
||
"frequency_mhz": cpu_freq.current if cpu_freq else 0,
|
||
"load_average": {
|
||
"1min": load_avg[0],
|
||
"5min": load_avg[1],
|
||
"15min": load_avg[2]
|
||
}
|
||
},
|
||
"disk": {
|
||
"read_bytes": disk_io.read_bytes if disk_io else 0,
|
||
"write_bytes": disk_io.write_bytes if disk_io else 0,
|
||
"read_count": disk_io.read_count if disk_io else 0,
|
||
"write_count": disk_io.write_count if disk_io else 0
|
||
},
|
||
"network": {
|
||
"bytes_sent": net_io.bytes_sent if net_io else 0,
|
||
"bytes_recv": net_io.bytes_recv if net_io else 0,
|
||
"packets_sent": net_io.packets_sent if net_io else 0,
|
||
"packets_recv": net_io.packets_recv if net_io else 0
|
||
},
|
||
"processes": processes
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"시스템 성능 정보 수집 실패: {e}")
|
||
return {}
|
||
|
||
def _get_api_statistics(self) -> Dict[str, Any]:
|
||
"""API 통계 정보를 반환합니다."""
|
||
# 실제 구현에서는 API 엔드포인트에서 이 정보를 수집해야 합니다
|
||
return {
|
||
"total_requests": self.api_stats["total_requests"],
|
||
"successful_requests": self.api_stats["successful_requests"],
|
||
"failed_requests": self.api_stats["failed_requests"],
|
||
"success_rate": (
|
||
(self.api_stats["successful_requests"] / max(self.api_stats["total_requests"], 1)) * 100
|
||
),
|
||
"endpoint_usage": self.api_stats["endpoint_usage"],
|
||
"average_response_time": (
|
||
sum(self.api_stats["response_times"]) / max(len(self.api_stats["response_times"]), 1)
|
||
) if self.api_stats["response_times"] else 0,
|
||
"recent_errors": self.api_stats["errors"][-5:] # 최근 5개 에러
|
||
}
|
||
|
||
def _check_alerts(self, worker_status: Dict) -> List[Dict]:
|
||
"""시스템 상태를 확인하고 알림을 생성합니다."""
|
||
alerts = []
|
||
current_time = datetime.now()
|
||
|
||
# 워커 상태 경고 - total_workers 필드 사용
|
||
total_workers = worker_status.get("total_workers", 0)
|
||
running = worker_status.get("running", False)
|
||
|
||
if not running:
|
||
alerts.append({
|
||
"level": "critical",
|
||
"message": "워커 매니저가 중지되었습니다",
|
||
"timestamp": current_time.isoformat(),
|
||
"category": "workers"
|
||
})
|
||
elif total_workers == 0:
|
||
alerts.append({
|
||
"level": "critical",
|
||
"message": "활성 워커가 없습니다",
|
||
"timestamp": current_time.isoformat(),
|
||
"category": "workers"
|
||
})
|
||
elif total_workers < 1:
|
||
alerts.append({
|
||
"level": "warning",
|
||
"message": f"워커 수가 부족합니다 (현재: {total_workers}개)",
|
||
"timestamp": current_time.isoformat(),
|
||
"category": "workers"
|
||
})
|
||
|
||
# Jetson 전용 경고
|
||
if settings.IS_JETSON:
|
||
# 온도 경고 (실제 구현에서는 온도 정보를 가져와야 함)
|
||
pass
|
||
|
||
return alerts
|
||
|
||
def update_api_stats(self, endpoint: str, success: bool, response_time: float, error: str = None):
|
||
"""API 통계를 업데이트합니다."""
|
||
self.api_stats["total_requests"] += 1
|
||
|
||
if success:
|
||
self.api_stats["successful_requests"] += 1
|
||
else:
|
||
self.api_stats["failed_requests"] += 1
|
||
if error:
|
||
self.api_stats["errors"].append({
|
||
"timestamp": datetime.now().isoformat(),
|
||
"endpoint": endpoint,
|
||
"error": error
|
||
})
|
||
|
||
# 엔드포인트별 사용량
|
||
if endpoint not in self.api_stats["endpoint_usage"]:
|
||
self.api_stats["endpoint_usage"][endpoint] = 0
|
||
self.api_stats["endpoint_usage"][endpoint] += 1
|
||
|
||
# 응답 시간
|
||
self.api_stats["response_times"].append(response_time)
|
||
if len(self.api_stats["response_times"]) > 100:
|
||
self.api_stats["response_times"].pop(0)
|
||
|
||
# 에러 로그 제한
|
||
if len(self.api_stats["errors"]) > 50:
|
||
self.api_stats["errors"] = self.api_stats["errors"][-50:]
|
||
|
||
def get_history(self) -> List[Dict[str, Any]]:
|
||
"""데이터 히스토리를 반환합니다."""
|
||
return self.history
|
||
|
||
def get_statistics(self) -> Dict[str, Any]:
|
||
"""통계 정보를 반환합니다."""
|
||
if not self.history:
|
||
return {}
|
||
|
||
recent_data = self.history[-10:] # 최근 10개 데이터
|
||
|
||
# GPU 사용률 평균
|
||
gpu_usage_avg = sum(d["gpu"]["usage_percent"] for d in recent_data) / len(recent_data)
|
||
gpu_util_avg = sum(d["gpu"]["utilization"] for d in recent_data) / len(recent_data)
|
||
|
||
# 시스템 메모리 사용률 평균
|
||
sys_mem_avg = sum(d["system_memory"]["usage_percent"] for d in recent_data) / len(recent_data)
|
||
|
||
# 워커 수 평균
|
||
worker_avg = sum(d["workers"]["active_workers"] for d in recent_data) / len(recent_data)
|
||
|
||
return {
|
||
"gpu_usage_avg": round(gpu_usage_avg, 2),
|
||
"gpu_util_avg": round(gpu_util_avg, 2),
|
||
"system_memory_avg": round(sys_mem_avg, 2),
|
||
"worker_avg": round(worker_avg, 2),
|
||
"data_points": len(recent_data)
|
||
}
|
||
|
||
def _get_default_worker_status(self):
|
||
return {
|
||
"total_workers": 0,
|
||
"queue_size": 0,
|
||
"workers_by_status": {"idle": [], "busy": [], "starting": [], "stopping": [], "error": []},
|
||
"running": False
|
||
}
|
||
|
||
def _get_default_session_status(self):
|
||
return {
|
||
"simple-lama": {"total": 0, "in_use": 0, "available": 0},
|
||
"migan": {"total": 0, "in_use": 0, "available": 0},
|
||
"rembg": {"total": 0, "in_use": 0, "available": 0}
|
||
}
|
||
|
||
|
||
# 전역 모니터링 데이터 인스턴스
|
||
monitoring_data = MonitoringData()
|
||
|
||
|
||
# HTML 템플릿
|
||
HTML_TEMPLATE = """
|
||
<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>인페인팅 서버 모니터링</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
<style>
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
margin: 0;
|
||
padding: 20px;
|
||
background-color: #f5f5f5;
|
||
}
|
||
.container {
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
}
|
||
.header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
}
|
||
.header h1 {
|
||
margin: 0;
|
||
font-size: 2.5em;
|
||
}
|
||
.header p {
|
||
margin: 10px 0 0 0;
|
||
opacity: 0.9;
|
||
}
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
.card {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
transition: transform 0.2s;
|
||
}
|
||
.card:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
.card h3 {
|
||
margin-top: 0;
|
||
color: #333;
|
||
border-bottom: 2px solid #667eea;
|
||
padding-bottom: 10px;
|
||
}
|
||
.metric {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin: 15px 0;
|
||
padding: 10px;
|
||
background: #f8f9fa;
|
||
border-radius: 5px;
|
||
}
|
||
.metric-label {
|
||
font-weight: 500;
|
||
color: #555;
|
||
}
|
||
.metric-value {
|
||
font-weight: bold;
|
||
font-size: 1.1em;
|
||
color: #667eea;
|
||
}
|
||
.chart-container {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
margin-bottom: 20px;
|
||
}
|
||
.chart-container h3 {
|
||
margin-top: 0;
|
||
color: #333;
|
||
border-bottom: 2px solid #667eea;
|
||
padding-bottom: 10px;
|
||
}
|
||
.alerts {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
margin-bottom: 20px;
|
||
}
|
||
.alert {
|
||
padding: 10px;
|
||
margin: 10px 0;
|
||
border-radius: 5px;
|
||
border-left: 4px solid;
|
||
}
|
||
.alert.critical {
|
||
background: #ffe6e6;
|
||
border-left-color: #dc3545;
|
||
color: #721c24;
|
||
}
|
||
.alert.warning {
|
||
background: #fff3cd;
|
||
border-left-color: #ffc107;
|
||
color: #856404;
|
||
}
|
||
.alert.info {
|
||
background: #d1ecf1;
|
||
border-left-color: #17a2b8;
|
||
color: #0c5460;
|
||
}
|
||
.endpoint-performance {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 15px;
|
||
margin-top: 15px;
|
||
}
|
||
.endpoint-item {
|
||
background: #f8f9fa;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
border-left: 4px solid #667eea;
|
||
}
|
||
.endpoint-name {
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
font-size: 1.1em;
|
||
}
|
||
.endpoint-metrics {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 8px;
|
||
font-size: 0.9em;
|
||
}
|
||
.endpoint-metric {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 4px 0;
|
||
}
|
||
.endpoint-metric-label {
|
||
color: #666;
|
||
}
|
||
.endpoint-metric-value {
|
||
color: #667eea;
|
||
font-weight: 600;
|
||
}
|
||
.system-info {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 15px;
|
||
}
|
||
.status-indicator {
|
||
display: inline-block;
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
}
|
||
.status-online { background: #28a745; }
|
||
.status-offline { background: #dc3545; }
|
||
.status-warning { background: #ffc107; }
|
||
.refresh-time {
|
||
text-align: center;
|
||
color: #666;
|
||
font-size: 0.9em;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.status {
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
font-size: 0.8em;
|
||
}
|
||
|
||
.status.connected {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.status.connecting {
|
||
background-color: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.status.reconnecting {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
animation: pulse 1s infinite;
|
||
}
|
||
|
||
.status.disconnected {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.status.error {
|
||
background-color: #f5c6cb;
|
||
color: #491217;
|
||
}
|
||
|
||
.status.failed {
|
||
background-color: #d1ecf1;
|
||
color: #0c5460;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
100% { opacity: 1; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🚀 인페인팅 서버 모니터링</h1>
|
||
<p>실시간 서버 상태 및 성능 모니터링 대시보드</p>
|
||
</div>
|
||
|
||
<!-- 시스템 개요 -->
|
||
<div class="grid">
|
||
<div class="card">
|
||
<h3>🖥️ 시스템 정보</h3>
|
||
<div class="metric">
|
||
<span class="metric-label">시스템 타입:</span>
|
||
<span class="metric-value" id="system-type">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">CPU 사용률:</span>
|
||
<span class="metric-value" id="cpu-usage">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">시스템 메모리:</span>
|
||
<span class="metric-value" id="system-memory">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">프로세스 수:</span>
|
||
<span class="metric-value" id="process-count">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>🎮 GPU 상태</h3>
|
||
<div class="metric">
|
||
<span class="metric-label">GPU 메모리:</span>
|
||
<span class="metric-value" id="gpu-memory">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">GPU 사용률:</span>
|
||
<span class="metric-value" id="gpu-util">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">GPU 온도:</span>
|
||
<span class="metric-value" id="gpu-temp">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">GPU 클럭:</span>
|
||
<span class="metric-value" id="gpu-clock">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>⚙️ 워커 상태</h3>
|
||
<div class="metric">
|
||
<span class="metric-label">활성 워커:</span>
|
||
<span class="metric-value" id="worker-count">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">대기열:</span>
|
||
<span class="metric-value" id="queue-size">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">상태:</span>
|
||
<span class="metric-value" id="worker-status">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>🔄 세션 풀 상태</h3>
|
||
<div class="metric">
|
||
<span class="metric-label">Simple LAMA:</span>
|
||
<span class="metric-value" id="session-lama">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">MIGAN:</span>
|
||
<span class="metric-value" id="session-migan">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">RemBG:</span>
|
||
<span class="metric-value" id="session-rembg">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">총 세션:</span>
|
||
<span class="metric-value" id="session-total">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>📊 API 통계</h3>
|
||
<div class="metric">
|
||
<span class="metric-label">총 요청:</span>
|
||
<span class="metric-value" id="total-requests">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">성공률:</span>
|
||
<span class="metric-value" id="success-rate">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">평균 응답시간:</span>
|
||
<span class="metric-value" id="avg-response-time">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">에러 수:</span>
|
||
<span class="metric-value" id="error-count">-</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 시스템 성능 상세 -->
|
||
<div class="card">
|
||
<h3>🔍 시스템 성능 상세</h3>
|
||
<div class="system-info">
|
||
<div>
|
||
<h4>CPU 정보</h4>
|
||
<div class="metric">
|
||
<span class="metric-label">코어 수:</span>
|
||
<span class="metric-value" id="cpu-count">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">클럭 속도:</span>
|
||
<span class="metric-value" id="cpu-freq">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">부하 평균 (1분):</span>
|
||
<span class="metric-value" id="load-1min">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">부하 평균 (5분):</span>
|
||
<span class="metric-value" id="load-5min">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h4>디스크 I/O</h4>
|
||
<div class="metric">
|
||
<span class="metric-label">읽기 (MB/s):</span>
|
||
<span class="metric-value" id="disk-read">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">쓰기 (MB/s):</span>
|
||
<span class="metric-value" id="disk-write">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">읽기 횟수:</span>
|
||
<span class="metric-value" id="disk-read-count">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">쓰기 횟수:</span>
|
||
<span class="metric-value" id="disk-write-count">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h4>네트워크 I/O</h4>
|
||
<div class="metric">
|
||
<span class="metric-label">송신 (MB):</span>
|
||
<span class="metric-value" id="net-sent">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">수신 (MB):</span>
|
||
<span class="metric-value" id="net-recv">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">송신 패킷:</span>
|
||
<span class="metric-value" id="net-sent-pkts">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">수신 패킷:</span>
|
||
<span class="metric-value" id="net-recv-pkts">-</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 엔드포인트 사용량 및 성능 -->
|
||
<div class="card">
|
||
<h3>🌐 엔드포인트 분석</h3>
|
||
<div class="endpoint-performance" id="endpoint-performance">
|
||
<div class="endpoint-item">
|
||
<div class="endpoint-name">로딩 중...</div>
|
||
<div class="endpoint-metrics">-</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 성능 지표 -->
|
||
<div class="card">
|
||
<h3>⚡ 성능 지표</h3>
|
||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));">
|
||
<div class="metric">
|
||
<span class="metric-label">초당 요청:</span>
|
||
<span class="metric-value" id="requests-per-second">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">동시 요청:</span>
|
||
<span class="metric-value" id="current-concurrent">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">최대 동시:</span>
|
||
<span class="metric-value" id="max-concurrent">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">최소 응답시간:</span>
|
||
<span class="metric-value" id="min-response-time">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">최대 응답시간:</span>
|
||
<span class="metric-value" id="max-response-time">-</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">서버 가동시간:</span>
|
||
<span class="metric-value" id="server-uptime">-</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 알림 및 경고 -->
|
||
<div class="alerts">
|
||
<h3>⚠️ 알림 및 경고</h3>
|
||
<div id="alerts-container">
|
||
<div class="alert info">
|
||
<strong>정보:</strong> 모니터링 데이터를 수집 중입니다...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 로그 뷰어 -->
|
||
<div class="card">
|
||
<h3>📝 최근 로그 (최근 50줄)</h3>
|
||
<div style="background: #f8f9fa; border-radius: 5px; padding: 15px; height: 300px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 12px;" id="logs-container">
|
||
로딩 중...
|
||
</div>
|
||
<div style="margin-top: 10px; text-align: right;">
|
||
<button onclick="refreshLogs()" style="padding: 5px 15px; background: #667eea; color: white; border: none; border-radius: 3px; cursor: pointer;">새로고침</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 성능 통계 -->
|
||
<div class="card">
|
||
<h3>⚡ 모델 로딩 성능 통계</h3>
|
||
<div id="performance-stats-container">
|
||
로딩 중...
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 모델 사용 통계 -->
|
||
<div class="card">
|
||
<h3>📊 모델별 사용 통계</h3>
|
||
<div id="model-usage-stats-container">
|
||
로딩 중...
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 시스템 경고 -->
|
||
<div class="card">
|
||
<h3>🚨 실시간 시스템 경고</h3>
|
||
<div id="system-alerts-container">
|
||
로딩 중...
|
||
</div>
|
||
<div style="margin-top: 10px; text-align: right;">
|
||
<button onclick="refreshSystemAlerts()" style="padding: 5px 15px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">경고 새로고침</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 모델 처리 시간 통계 -->
|
||
<div class="card">
|
||
<h3>⏱️ 모델 처리 시간 통계 (초)</h3>
|
||
<table id="model-perf-table" style="width: 100%; border-collapse: collapse; margin-top: 15px;">
|
||
<thead>
|
||
<tr style="text-align: left; background: #f8f9fa;">
|
||
<th style="padding: 10px; border-bottom: 2px solid #ddd;">모델</th>
|
||
<th style="padding: 10px; border-bottom: 2px solid #ddd;">처리 횟수</th>
|
||
<th style="padding: 10px; border-bottom: 2px solid #ddd;">평균 시간</th>
|
||
<th style="padding: 10px; border-bottom: 2px solid #ddd;">최소 시간</th>
|
||
<th style="padding: 10px; border-bottom: 2px solid #ddd;">최대 시간</th>
|
||
<th style="padding: 10px; border-bottom: 2px solid #ddd;">총 시간</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<!-- JS로 채워질 내용 -->
|
||
</tbody>
|
||
</table>
|
||
<div style="margin-top: 10px; text-align: right;">
|
||
<button onclick="resetPerformanceStats()" style="padding: 5px 15px; background: #ffc107; color: #212529; border: none; border-radius: 3px; cursor: pointer;">통계 초기화</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 차트 -->
|
||
<div class="chart-container">
|
||
<h3>📈 실시간 성능 차트</h3>
|
||
<canvas id="performanceChart" width="400" height="200"></canvas>
|
||
</div>
|
||
|
||
<div class="chart-container">
|
||
<h3>🎯 GPU 메모리 사용량</h3>
|
||
<canvas id="gpuChart" width="400" height="200"></canvas>
|
||
</div>
|
||
|
||
<div class="refresh-time">
|
||
마지막 업데이트: <span id="last-update">-</span> |
|
||
연결 상태: <span id="connection-status" class="status connecting">연결 중...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 차트 초기화
|
||
const performanceCtx = document.getElementById('performanceChart').getContext('2d');
|
||
const gpuCtx = document.getElementById('gpuChart').getContext('2d');
|
||
|
||
const performanceChart = new Chart(performanceCtx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [],
|
||
datasets: [{
|
||
label: 'GPU 사용률 (%)',
|
||
data: [],
|
||
borderColor: 'rgb(75, 192, 192)',
|
||
tension: 0.1
|
||
}, {
|
||
label: '시스템 메모리 (%)',
|
||
data: [],
|
||
borderColor: 'rgb(255, 99, 132)',
|
||
tension: 0.1
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
max: 100
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const gpuChart = new Chart(gpuCtx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [],
|
||
datasets: [{
|
||
label: 'GPU 메모리 사용률 (%)',
|
||
data: [],
|
||
borderColor: 'rgb(54, 162, 235)',
|
||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||
tension: 0.1
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
max: 100
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// WebSocket 연결 관리
|
||
let ws;
|
||
let reconnectAttempts = 0;
|
||
const maxReconnectAttempts = 5;
|
||
const reconnectInterval = 3000; // 3초
|
||
let lastHeartbeat = Date.now();
|
||
const heartbeatTimeout = 10000; // 10초 타임아웃
|
||
|
||
function connectWebSocket() {
|
||
try {
|
||
ws = new WebSocket(`ws://${window.location.host}/ws`);
|
||
|
||
ws.onopen = function() {
|
||
console.log('WebSocket 연결이 성공했습니다.');
|
||
reconnectAttempts = 0;
|
||
// 연결 상태 표시 업데이트
|
||
document.getElementById('connection-status').textContent = '연결됨';
|
||
document.getElementById('connection-status').className = 'status connected';
|
||
};
|
||
|
||
ws.onmessage = function(event) {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
|
||
// heartbeat 체크
|
||
if (data.heartbeat) {
|
||
lastHeartbeat = Date.now();
|
||
}
|
||
|
||
updateDashboard(data);
|
||
} catch (e) {
|
||
console.error('데이터 파싱 오류:', e);
|
||
}
|
||
};
|
||
|
||
ws.onclose = function(event) {
|
||
console.log(`WebSocket 연결이 종료되었습니다. 코드: ${event.code}, 이유: ${event.reason}`);
|
||
document.getElementById('connection-status').textContent = '연결 끊어짐';
|
||
document.getElementById('connection-status').className = 'status disconnected';
|
||
|
||
// 자동 재연결 시도
|
||
if (reconnectAttempts < maxReconnectAttempts) {
|
||
reconnectAttempts++;
|
||
console.log(`재연결 시도 ${reconnectAttempts}/${maxReconnectAttempts} in ${reconnectInterval/1000}초...`);
|
||
document.getElementById('connection-status').textContent = `재연결 중... (${reconnectAttempts}/${maxReconnectAttempts})`;
|
||
document.getElementById('connection-status').className = 'status reconnecting';
|
||
|
||
setTimeout(connectWebSocket, reconnectInterval);
|
||
} else {
|
||
console.log('최대 재연결 시도 횟수를 초과했습니다. 페이지를 새로고침합니다.');
|
||
document.getElementById('connection-status').textContent = '연결 실패';
|
||
document.getElementById('connection-status').className = 'status failed';
|
||
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 5000);
|
||
}
|
||
};
|
||
|
||
ws.onerror = function(error) {
|
||
console.error('WebSocket 오류:', error);
|
||
document.getElementById('connection-status').textContent = '연결 오류';
|
||
document.getElementById('connection-status').className = 'status error';
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('WebSocket 연결 생성 오류:', error);
|
||
document.getElementById('connection-status').textContent = '연결 생성 실패';
|
||
document.getElementById('connection-status').className = 'status error';
|
||
}
|
||
}
|
||
|
||
// 페이지 가시성 변경 감지
|
||
document.addEventListener('visibilitychange', function() {
|
||
if (document.visibilityState === 'visible') {
|
||
// 탭이 다시 활성화되면 연결 상태 확인
|
||
if (ws.readyState !== WebSocket.OPEN) {
|
||
console.log('탭이 활성화되어 WebSocket 재연결을 시도합니다.');
|
||
connectWebSocket();
|
||
}
|
||
}
|
||
});
|
||
|
||
// 초기 연결
|
||
connectWebSocket();
|
||
|
||
// heartbeat 모니터링
|
||
setInterval(function() {
|
||
const now = Date.now();
|
||
if (now - lastHeartbeat > heartbeatTimeout && ws && ws.readyState === WebSocket.OPEN) {
|
||
console.log('heartbeat 타임아웃 - 연결을 다시 시도합니다.');
|
||
ws.close();
|
||
}
|
||
}, 5000); // 5초마다 체크
|
||
|
||
function updateDashboard(data) {
|
||
console.log("받은 데이터:", data); // 디버깅용 로그 추가
|
||
|
||
// 기본 메트릭 업데이트
|
||
document.getElementById('system-type').textContent = data.system_type || '-';
|
||
document.getElementById('cpu-usage').textContent = (data.system_performance?.cpu?.usage_percent || 0).toFixed(1) + '%';
|
||
document.getElementById('system-memory').textContent = (data.system_memory?.usage_percent || 0).toFixed(1) + '%';
|
||
document.getElementById('process-count').textContent = data.system_performance?.processes || '-';
|
||
|
||
// GPU 정보 업데이트
|
||
document.getElementById('gpu-memory').textContent = (data.gpu?.usage_percent || 0).toFixed(1) + '%';
|
||
document.getElementById('gpu-util').textContent = (data.gpu?.utilization || 0).toFixed(1) + '%';
|
||
|
||
// GPU 온도 표시 (지원 여부에 따라)
|
||
const gpuTemp = data.gpu?.temperature;
|
||
if (typeof gpuTemp === 'number') {
|
||
document.getElementById('gpu-temp').textContent = gpuTemp.toFixed(1) + '°C';
|
||
} else {
|
||
document.getElementById('gpu-temp').textContent = gpuTemp || '미지원';
|
||
}
|
||
|
||
// GPU 클럭 표시 (지원 여부에 따라)
|
||
const gpuClock = data.gpu?.clock_speed;
|
||
if (typeof gpuClock === 'number') {
|
||
document.getElementById('gpu-clock').textContent = gpuClock.toFixed(0) + 'MHz';
|
||
} else {
|
||
document.getElementById('gpu-clock').textContent = gpuClock || '미지원';
|
||
}
|
||
|
||
// 워커 정보 업데이트
|
||
if (data.workers) {
|
||
document.getElementById('worker-count').textContent = data.workers.total_workers || 0;
|
||
document.getElementById('queue-size').textContent = data.workers.queue_size || 0;
|
||
document.getElementById('worker-status').textContent = data.workers.running ? '실행 중' : '중지됨';
|
||
|
||
// 워커 상태별 상세 정보 표시 (디버깅용)
|
||
console.log("워커 상태:", data.workers);
|
||
if (data.workers.workers_by_status) {
|
||
const idleCount = data.workers.workers_by_status.idle?.length || 0;
|
||
const busyCount = data.workers.workers_by_status.busy?.length || 0;
|
||
console.log(`유휴 워커: ${idleCount}, 작업 중 워커: ${busyCount}`);
|
||
}
|
||
}
|
||
|
||
// 세션 풀 상세 정보 업데이트
|
||
if (data.sessions) {
|
||
// Simple LAMA 세션
|
||
const lamaSession = data.sessions['simple-lama'] || data.sessions['simple_lama'] || {};
|
||
const lamaTotalSessions = lamaSession.total || 0;
|
||
const lamaInUse = lamaSession.in_use || 0;
|
||
const lamaAvailable = lamaSession.available || (lamaTotalSessions - lamaInUse);
|
||
document.getElementById('session-lama').textContent = `${lamaInUse}/${lamaTotalSessions} 사용중`;
|
||
|
||
// MIGAN 세션
|
||
const miganSession = data.sessions['migan'] || {};
|
||
const miganTotalSessions = miganSession.total || 0;
|
||
const miganInUse = miganSession.in_use || 0;
|
||
const miganAvailable = miganSession.available || (miganTotalSessions - miganInUse);
|
||
document.getElementById('session-migan').textContent = `${miganInUse}/${miganTotalSessions} 사용중`;
|
||
|
||
// RemBG 세션
|
||
const rembgSession = data.sessions['rembg'] || {};
|
||
const rembgTotalSessions = rembgSession.total || 0;
|
||
const rembgInUse = rembgSession.in_use || 0;
|
||
const rembgAvailable = rembgSession.available || (rembgTotalSessions - rembgInUse);
|
||
document.getElementById('session-rembg').textContent = `${rembgInUse}/${rembgTotalSessions} 사용중`;
|
||
|
||
// 총 세션 수
|
||
const totalSessions = lamaTotalSessions + miganTotalSessions + rembgTotalSessions;
|
||
const totalInUse = lamaInUse + miganInUse + rembgInUse;
|
||
document.getElementById('session-total').textContent = `${totalInUse}/${totalSessions} 사용중`;
|
||
|
||
console.log("세션 풀 상태:", data.sessions);
|
||
} else {
|
||
// 세션 데이터가 없는 경우 기본값 표시
|
||
document.getElementById('session-lama').textContent = '0/0 사용중';
|
||
document.getElementById('session-migan').textContent = '0/0 사용중';
|
||
document.getElementById('session-rembg').textContent = '0/0 사용중';
|
||
document.getElementById('session-total').textContent = '0/0 사용중';
|
||
}
|
||
|
||
// API 통계 업데이트
|
||
if (data.api_stats) {
|
||
document.getElementById('total-requests').textContent = data.api_stats.total_requests || 0;
|
||
document.getElementById('success-rate').textContent = (data.api_stats.success_rate || 0).toFixed(1) + '%';
|
||
document.getElementById('avg-response-time').textContent = (data.api_stats.average_response_time || 0).toFixed(2) + 'ms';
|
||
document.getElementById('error-count').textContent = data.api_stats.failed_requests || 0;
|
||
|
||
// 새로운 성능 지표 업데이트
|
||
document.getElementById('requests-per-second').textContent = (data.api_stats.requests_per_second || 0).toFixed(2) + '/s';
|
||
document.getElementById('current-concurrent').textContent = data.api_stats.current_concurrent || 0;
|
||
document.getElementById('max-concurrent').textContent = data.api_stats.max_concurrent || 0;
|
||
document.getElementById('min-response-time').textContent = (data.api_stats.min_response_time || 0).toFixed(3) + 'ms';
|
||
document.getElementById('max-response-time').textContent = (data.api_stats.max_response_time || 0).toFixed(3) + 'ms';
|
||
|
||
// 서버 가동시간 포맷팅
|
||
const uptime = data.api_stats.uptime || 0;
|
||
const hours = Math.floor(uptime / 3600);
|
||
const minutes = Math.floor((uptime % 3600) / 60);
|
||
const seconds = Math.floor(uptime % 60);
|
||
document.getElementById('server-uptime').textContent = `${hours}h ${minutes}m ${seconds}s`;
|
||
}
|
||
|
||
// 시스템 성능 상세 업데이트
|
||
if (data.system_performance?.cpu) {
|
||
document.getElementById('cpu-count').textContent = data.system_performance.cpu.count || '-';
|
||
document.getElementById('cpu-freq').textContent = (data.system_performance.cpu.frequency_mhz || 0).toFixed(0) + 'MHz';
|
||
document.getElementById('load-1min').textContent = (data.system_performance.cpu.load_average?.['1min'] || 0).toFixed(2);
|
||
document.getElementById('load-5min').textContent = (data.system_performance.cpu.load_average?.['5min'] || 0).toFixed(2);
|
||
}
|
||
|
||
if (data.system_performance?.disk) {
|
||
document.getElementById('disk-read').textContent = ((data.system_performance.disk.read_bytes || 0) / 1024 / 1024).toFixed(2);
|
||
document.getElementById('disk-write').textContent = ((data.system_performance.disk.write_bytes || 0) / 1024 / 1024).toFixed(2);
|
||
document.getElementById('disk-read-count').textContent = data.system_performance.disk.read_count || '-';
|
||
document.getElementById('disk-write-count').textContent = data.system_performance.disk.write_count || '-';
|
||
}
|
||
|
||
if (data.system_performance?.network) {
|
||
document.getElementById('net-sent').textContent = ((data.system_performance.network.bytes_sent || 0) / 1024 / 1024).toFixed(2);
|
||
document.getElementById('net-recv').textContent = ((data.system_performance.network.bytes_recv || 0) / 1024 / 1024).toFixed(2);
|
||
document.getElementById('net-sent-pkts').textContent = data.system_performance.network.packets_sent || '-';
|
||
document.getElementById('net-recv-pkts').textContent = data.system_performance.network.packets_recv || '-';
|
||
}
|
||
|
||
// 엔드포인트 성능 분석 업데이트
|
||
updateEndpointPerformance(data.api_stats?.endpoint_stats || {});
|
||
|
||
// 모델 처리 시간 통계 업데이트
|
||
updateModelPerformance(data.model_performance_stats || {});
|
||
|
||
// 알림 업데이트
|
||
updateAlerts(data.alerts || []);
|
||
|
||
// 차트 업데이트
|
||
updateCharts(data);
|
||
|
||
// 마지막 업데이트 시간
|
||
document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
|
||
}
|
||
|
||
function updateEndpointPerformance(endpointStats) {
|
||
const container = document.getElementById('endpoint-performance');
|
||
container.innerHTML = '';
|
||
|
||
if (Object.keys(endpointStats).length === 0) {
|
||
container.innerHTML = '<div class="endpoint-item"><div class="endpoint-name">활동 없음</div><div class="endpoint-metrics">데이터 수집 중...</div></div>';
|
||
return;
|
||
}
|
||
|
||
Object.entries(endpointStats).forEach(([endpoint, stats]) => {
|
||
const item = document.createElement('div');
|
||
item.className = 'endpoint-item';
|
||
|
||
// 엔드포인트 이름 단축
|
||
const shortEndpoint = endpoint.length > 40 ? endpoint.substring(0, 37) + '...' : endpoint;
|
||
|
||
item.innerHTML = `
|
||
<div class="endpoint-name">${shortEndpoint}</div>
|
||
<div class="endpoint-metrics">
|
||
<div class="endpoint-metric">
|
||
<span class="endpoint-metric-label">요청 수:</span>
|
||
<span class="endpoint-metric-value">${stats.count}</span>
|
||
</div>
|
||
<div class="endpoint-metric">
|
||
<span class="endpoint-metric-label">평균:</span>
|
||
<span class="endpoint-metric-value">${stats.avg_time.toFixed(2)}ms</span>
|
||
</div>
|
||
<div class="endpoint-metric">
|
||
<span class="endpoint-metric-label">최소:</span>
|
||
<span class="endpoint-metric-value">${stats.min_time.toFixed(2)}ms</span>
|
||
</div>
|
||
<div class="endpoint-metric">
|
||
<span class="endpoint-metric-label">최대:</span>
|
||
<span class="endpoint-metric-value">${stats.max_time.toFixed(2)}ms</span>
|
||
</div>
|
||
<div class="endpoint-metric">
|
||
<span class="endpoint-metric-label">진행 중:</span>
|
||
<span class="endpoint-metric-value">${stats.current_concurrent}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
container.appendChild(item);
|
||
});
|
||
}
|
||
|
||
function updateAlerts(alerts) {
|
||
const container = document.getElementById('alerts-container');
|
||
container.innerHTML = '';
|
||
|
||
if (alerts.length === 0) {
|
||
container.innerHTML = '<div class="alert info"><strong>정보:</strong> 현재 알림이 없습니다.</div>';
|
||
return;
|
||
}
|
||
|
||
alerts.forEach(alert => {
|
||
const alertDiv = document.createElement('div');
|
||
alertDiv.className = `alert ${alert.level}`;
|
||
alertDiv.innerHTML = `
|
||
<strong>${alert.level.toUpperCase()}:</strong> ${alert.message}
|
||
<br><small>${new Date(alert.timestamp).toLocaleString()}</small>
|
||
`;
|
||
container.appendChild(alertDiv);
|
||
});
|
||
}
|
||
|
||
function updateCharts(data) {
|
||
const timestamp = new Date().toLocaleTimeString();
|
||
|
||
// 성능 차트 업데이트
|
||
performanceChart.data.labels.push(timestamp);
|
||
performanceChart.data.datasets[0].data.push(data.gpu?.utilization || 0);
|
||
performanceChart.data.datasets[1].data.push(data.system_memory?.usage_percent || 0);
|
||
|
||
if (performanceChart.data.labels.length > 20) {
|
||
performanceChart.data.labels.shift();
|
||
performanceChart.data.datasets[0].data.shift();
|
||
performanceChart.data.datasets[1].data.shift();
|
||
}
|
||
|
||
// GPU 차트 업데이트
|
||
gpuChart.data.labels.push(timestamp);
|
||
gpuChart.data.datasets[0].data.push(data.gpu?.usage_percent || 0);
|
||
|
||
if (gpuChart.data.labels.length > 20) {
|
||
gpuChart.data.labels.shift();
|
||
gpuChart.data.datasets[0].data.shift();
|
||
}
|
||
|
||
performanceChart.update();
|
||
gpuChart.update();
|
||
}
|
||
|
||
function updateModelPerformance(stats) {
|
||
const tableBody = document.querySelector('#model-perf-table tbody');
|
||
tableBody.innerHTML = ''; // 기존 내용 삭제
|
||
|
||
if (Object.keys(stats).length === 0) {
|
||
tableBody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 20px; color: #666;">데이터 수집 중...</td></tr>';
|
||
return;
|
||
}
|
||
|
||
for (const model in stats) {
|
||
if (stats.hasOwnProperty(model)) {
|
||
const modelStats = stats[model];
|
||
const row = `
|
||
<tr>
|
||
<td style="padding: 10px; border-bottom: 1px solid #eee;"><strong>${model}</strong></td>
|
||
<td style="padding: 10px; border-bottom: 1px solid #eee;">${modelStats.count}</td>
|
||
<td style="padding: 10px; border-bottom: 1px solid #eee;">${modelStats.avg_time}</td>
|
||
<td style="padding: 10px; border-bottom: 1px solid #eee;">${modelStats.min_time}</td>
|
||
<td style="padding: 10px; border-bottom: 1px solid #eee;">${modelStats.max_time}</td>
|
||
<td style="padding: 10px; border-bottom: 1px solid #eee;">${modelStats.total_time}</td>
|
||
</tr>
|
||
`;
|
||
tableBody.innerHTML += row;
|
||
}
|
||
}
|
||
}
|
||
|
||
function resetPerformanceStats() {
|
||
if (!confirm('정말로 모든 모델 처리 시간 통계를 초기화하시겠습니까?')) {
|
||
return;
|
||
}
|
||
fetch('/api/v1/stats/reset', { method: 'POST' })
|
||
.then(response => {
|
||
if (response.ok) {
|
||
console.log('Performance stats reset successfully.');
|
||
// 즉시 UI 업데이트를 위해 빈 객체 전달
|
||
updateModelPerformance({});
|
||
} else {
|
||
console.error('Failed to reset performance stats.');
|
||
}
|
||
})
|
||
.catch(error => console.error('Error resetting performance stats:', error));
|
||
}
|
||
|
||
// 로그 새로고침 함수
|
||
function refreshLogs() {
|
||
fetch('/api/logs?lines=50')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const container = document.getElementById('logs-container');
|
||
if (data.logs && data.logs.length > 0) {
|
||
let logHtml = '';
|
||
data.logs.forEach(log => {
|
||
if (log.raw) {
|
||
logHtml += `<div style="margin-bottom: 3px; color: #666;">${escapeHtml(log.raw)}</div>`;
|
||
} else {
|
||
const levelColor = getLevelColor(log.level);
|
||
logHtml += `<div style="margin-bottom: 3px;">
|
||
<span style="color: #888; font-size: 11px;">${log.timestamp}</span>
|
||
<span style="color: ${levelColor}; font-weight: bold; margin: 0 5px;">[${log.level}]</span>
|
||
<span style="color: #666; margin-right: 5px;">${log.module}:</span>
|
||
<span>${escapeHtml(log.message)}</span>
|
||
</div>`;
|
||
}
|
||
});
|
||
container.innerHTML = logHtml;
|
||
container.scrollTop = container.scrollHeight; // 스크롤을 맨 아래로
|
||
} else {
|
||
container.innerHTML = '<div style="color: #999;">로그가 없습니다.</div>';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('로그 로딩 실패:', error);
|
||
document.getElementById('logs-container').innerHTML = '<div style="color: #dc3545;">로그 로딩 실패</div>';
|
||
});
|
||
}
|
||
|
||
// 성능 통계 새로고침 함수
|
||
function refreshPerformanceStats() {
|
||
fetch('/api/performance-stats')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const container = document.getElementById('performance-stats-container');
|
||
if (data.stats && Object.keys(data.stats).length > 0) {
|
||
let statsHtml = '<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;">';
|
||
|
||
Object.entries(data.stats).forEach(([modelName, stats]) => {
|
||
statsHtml += `<div style="background: #f8f9fa; padding: 15px; border-radius: 5px;">
|
||
<h4 style="margin-top: 0; color: #667eea;">${modelName}</h4>
|
||
<div class="metric">
|
||
<span class="metric-label">평균 로딩 시간:</span>
|
||
<span class="metric-value">${stats.avg_ms.toFixed(1)}ms</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">최소/최대:</span>
|
||
<span class="metric-value">${stats.min_ms.toFixed(1)}ms / ${stats.max_ms.toFixed(1)}ms</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">총 로딩 횟수:</span>
|
||
<span class="metric-value">${stats.count}회</span>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
|
||
statsHtml += '</div>';
|
||
container.innerHTML = statsHtml;
|
||
} else {
|
||
container.innerHTML = '<div style="color: #999; text-align: center; padding: 20px;">성능 데이터가 없습니다.</div>';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('성능 통계 로딩 실패:', error);
|
||
document.getElementById('performance-stats-container').innerHTML = '<div style="color: #dc3545;">성능 통계 로딩 실패</div>';
|
||
});
|
||
}
|
||
|
||
// 헬퍼 함수들
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function getLevelColor(level) {
|
||
switch(level) {
|
||
case 'ERROR': return '#dc3545';
|
||
case 'WARNING': return '#ffc107';
|
||
case 'INFO': return '#17a2b8';
|
||
case 'DEBUG': return '#6c757d';
|
||
default: return '#333';
|
||
}
|
||
}
|
||
|
||
// 모델 사용 통계 새로고침 함수
|
||
function refreshModelUsageStats() {
|
||
fetch('/api/model-usage-stats')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const container = document.getElementById('model-usage-stats-container');
|
||
if (data.model_usage && Object.keys(data.model_usage).length > 0) {
|
||
let statsHtml = '<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px;">';
|
||
|
||
// 모델별 사용량
|
||
statsHtml += '<div style="background: #e3f2fd; padding: 15px; border-radius: 5px;">';
|
||
statsHtml += '<h4 style="margin-top: 0; color: #1976d2;">🎯 모델별 요청</h4>';
|
||
Object.entries(data.model_usage).forEach(([model, count]) => {
|
||
statsHtml += `<div class="metric">
|
||
<span class="metric-label">${model}:</span>
|
||
<span class="metric-value">${count}회</span>
|
||
</div>`;
|
||
});
|
||
statsHtml += '</div>';
|
||
|
||
// 엔드포인트별 사용량
|
||
if (data.endpoint_usage && Object.keys(data.endpoint_usage).length > 0) {
|
||
statsHtml += '<div style="background: #f3e5f5; padding: 15px; border-radius: 5px;">';
|
||
statsHtml += '<h4 style="margin-top: 0; color: #7b1fa2;">🔗 엔드포인트별</h4>';
|
||
Object.entries(data.endpoint_usage).forEach(([endpoint, count]) => {
|
||
statsHtml += `<div class="metric">
|
||
<span class="metric-label">${endpoint}:</span>
|
||
<span class="metric-value">${count}회</span>
|
||
</div>`;
|
||
});
|
||
statsHtml += '</div>';
|
||
}
|
||
|
||
// 총 요청 수
|
||
statsHtml += '<div style="background: #e8f5e8; padding: 15px; border-radius: 5px;">';
|
||
statsHtml += '<h4 style="margin-top: 0; color: #388e3c;">📈 총계</h4>';
|
||
statsHtml += `<div class="metric">
|
||
<span class="metric-label">총 요청:</span>
|
||
<span class="metric-value">${data.total_requests}회</span>
|
||
</div>`;
|
||
statsHtml += `<div class="metric">
|
||
<span class="metric-label">분석 범위:</span>
|
||
<span class="metric-value">${data.analysis_window}</span>
|
||
</div>`;
|
||
statsHtml += '</div>';
|
||
|
||
statsHtml += '</div>';
|
||
container.innerHTML = statsHtml;
|
||
} else {
|
||
container.innerHTML = '<div style="color: #999; text-align: center; padding: 20px;">사용 데이터가 없습니다.</div>';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('모델 사용 통계 로딩 실패:', error);
|
||
document.getElementById('model-usage-stats-container').innerHTML = '<div style="color: #dc3545;">통계 로딩 실패</div>';
|
||
});
|
||
}
|
||
|
||
// 시스템 경고 새로고침 함수
|
||
function refreshSystemAlerts() {
|
||
fetch('/api/system-alerts')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const container = document.getElementById('system-alerts-container');
|
||
if (data.alerts && data.alerts.length > 0) {
|
||
let alertsHtml = '';
|
||
data.alerts.forEach(alert => {
|
||
const levelColor = alert.level === 'critical' ? '#dc3545' :
|
||
alert.level === 'warning' ? '#ffc107' : '#28a745';
|
||
const levelIcon = alert.level === 'critical' ? '🚨' :
|
||
alert.level === 'warning' ? '⚠️' : 'ℹ️';
|
||
|
||
alertsHtml += `<div style="background: ${levelColor}20; border-left: 4px solid ${levelColor}; padding: 15px; margin-bottom: 10px; border-radius: 5px;">
|
||
<div style="font-weight: bold; color: ${levelColor};">
|
||
${levelIcon} ${alert.level.toUpperCase()}
|
||
</div>
|
||
<div style="margin-top: 5px;">${alert.message}</div>
|
||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||
${new Date(alert.timestamp).toLocaleString()}
|
||
</div>
|
||
</div>`;
|
||
});
|
||
container.innerHTML = alertsHtml;
|
||
} else {
|
||
container.innerHTML = '<div style="color: #28a745; text-align: center; padding: 20px;">✅ 현재 시스템 경고가 없습니다.</div>';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('시스템 경고 로딩 실패:', error);
|
||
document.getElementById('system-alerts-container').innerHTML = '<div style="color: #dc3545;">경고 로딩 실패</div>';
|
||
});
|
||
}
|
||
|
||
// 페이지 로드 시 초기화
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 로그 및 성능 통계 초기 로딩
|
||
refreshLogs();
|
||
refreshPerformanceStats();
|
||
refreshModelUsageStats();
|
||
refreshSystemAlerts();
|
||
|
||
// 30초마다 로그 및 성능 통계 자동 새로고침
|
||
setInterval(refreshLogs, 30000);
|
||
setInterval(refreshPerformanceStats, 30000);
|
||
setInterval(refreshModelUsageStats, 15000); // 15초마다
|
||
setInterval(refreshSystemAlerts, 10000); // 10초마다
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
|
||
@monitor_app.get("/")
|
||
async def dashboard():
|
||
"""대시보드 HTML 페이지"""
|
||
return HTMLResponse(content=HTML_TEMPLATE)
|
||
|
||
|
||
@api_router.get("/status")
|
||
async def get_status():
|
||
"""실시간 서버 상태 데이터를 반환합니다."""
|
||
return await monitoring_data.collect_data()
|
||
|
||
# 간단한 상태 엔드포인트
|
||
@api_router.get("/simple")
|
||
async def get_simple_status():
|
||
"""간단한 서버 상태를 반환합니다."""
|
||
try:
|
||
# status.json 파일에서 상태 읽기
|
||
status = read_status_from_file()
|
||
|
||
# 시스템 메모리 정보
|
||
memory_info = gpu_monitor.get_system_memory_info()
|
||
|
||
return {
|
||
"timestamp": time.time(),
|
||
"system_type": "Jetson Xavier" if settings.IS_JETSON else "x86_64",
|
||
"cpu_percent": psutil.cpu_percent(interval=0.1),
|
||
"memory_percent": memory_info.get("usage_percent", 0),
|
||
"status": "running"
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"간단한 상태 조회 실패: {e}")
|
||
return {"error": str(e)}
|
||
|
||
@api_router.get("/logs")
|
||
async def get_recent_logs(lines: int = 100):
|
||
"""최근 로그 반환"""
|
||
try:
|
||
import os
|
||
log_file = "logs/main.log"
|
||
|
||
if not os.path.exists(log_file):
|
||
return {"logs": [], "message": "로그 파일이 없습니다"}
|
||
|
||
# 최근 lines 줄 읽기
|
||
with open(log_file, 'r', encoding='utf-8') as f:
|
||
all_lines = f.readlines()
|
||
recent_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
||
|
||
# 로그 파싱
|
||
parsed_logs = []
|
||
for line in recent_lines:
|
||
line = line.strip()
|
||
if line and " - " in line:
|
||
try:
|
||
# 시간, 모듈, 레벨, 메시지 분리
|
||
parts = line.split(" - ", 3)
|
||
if len(parts) >= 4:
|
||
parsed_logs.append({
|
||
"timestamp": parts[0],
|
||
"module": parts[1],
|
||
"level": parts[2],
|
||
"message": parts[3]
|
||
})
|
||
else:
|
||
parsed_logs.append({"raw": line})
|
||
except:
|
||
parsed_logs.append({"raw": line})
|
||
|
||
return {"logs": parsed_logs, "total": len(parsed_logs)}
|
||
|
||
except Exception as e:
|
||
logger.error(f"로그 조회 실패: {e}")
|
||
return {"logs": [], "error": str(e)}
|
||
|
||
@api_router.get("/model-usage-stats")
|
||
async def get_model_usage_stats():
|
||
"""모델별 사용 통계 반환"""
|
||
try:
|
||
import os
|
||
import re
|
||
from collections import defaultdict
|
||
from datetime import datetime, timedelta
|
||
|
||
log_file = "logs/main.log"
|
||
if not os.path.exists(log_file):
|
||
return {"stats": {}, "message": "로그 파일이 없습니다"}
|
||
|
||
# 최근 1시간 데이터만 분석
|
||
one_hour_ago = datetime.now() - timedelta(hours=1)
|
||
|
||
# 모델별 요청 통계
|
||
model_requests = defaultdict(int)
|
||
endpoint_requests = defaultdict(int)
|
||
hourly_requests = defaultdict(int)
|
||
|
||
with open(log_file, 'r', encoding='utf-8') as f:
|
||
lines = f.readlines()[-2000:] # 최근 2000줄만
|
||
|
||
for line in lines:
|
||
try:
|
||
# API 요청 로그 패턴
|
||
if '"POST /api/v1/inpaint' in line:
|
||
model_requests['inpaint'] += 1
|
||
elif '"POST /api/v1/remove_bg' in line:
|
||
model_requests['remove_bg'] += 1
|
||
elif '"GET /api/v1/model' in line:
|
||
endpoint_requests['health_check'] += 1
|
||
|
||
# 시간대별 분석
|
||
time_match = re.search(r'(\d{2}:\d{2}):\d{2}', line)
|
||
if time_match:
|
||
hour_minute = time_match.group(1)
|
||
hourly_requests[hour_minute] += 1
|
||
|
||
except Exception:
|
||
continue
|
||
|
||
return {
|
||
"model_usage": dict(model_requests),
|
||
"endpoint_usage": dict(endpoint_requests),
|
||
"hourly_distribution": dict(hourly_requests),
|
||
"total_requests": sum(model_requests.values()) + sum(endpoint_requests.values()),
|
||
"analysis_window": "최근 2000줄"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"모델 사용 통계 조회 실패: {e}")
|
||
return {"stats": {}, "error": str(e)}
|
||
|
||
@api_router.get("/performance-stats")
|
||
async def get_performance_stats():
|
||
"""성능 통계 반환"""
|
||
try:
|
||
import os
|
||
import re
|
||
from collections import defaultdict
|
||
|
||
log_file = "logs/main.log"
|
||
if not os.path.exists(log_file):
|
||
return {"stats": {}, "message": "로그 파일이 없습니다"}
|
||
|
||
# 모델 로딩 시간 분석
|
||
model_load_times = defaultdict(list)
|
||
|
||
# 최근 1000줄만 분석
|
||
with open(log_file, 'r', encoding='utf-8') as f:
|
||
lines = f.readlines()[-1000:]
|
||
|
||
# 모델 로딩 시간 분석
|
||
for i, line in enumerate(lines):
|
||
if "Loading" in line and "model" in line:
|
||
loading_time = None
|
||
success_time = None
|
||
|
||
# 현재 라인에서 시간 추출
|
||
try:
|
||
time_match = re.search(r'(\d{2}:\d{2}:\d{2}),(\d{3})', line)
|
||
if time_match:
|
||
loading_time = f"{time_match.group(1)}.{time_match.group(2)}"
|
||
|
||
# 다음 몇 줄에서 "loaded successfully" 찾기
|
||
for j in range(i+1, min(i+5, len(lines))):
|
||
if "loaded successfully" in lines[j]:
|
||
success_match = re.search(r'(\d{2}:\d{2}:\d{2}),(\d{3})', lines[j])
|
||
if success_match:
|
||
success_time = f"{success_match.group(1)}.{success_match.group(2)}"
|
||
|
||
# 시간 차이 계산 (초 단위)
|
||
loading_parts = loading_time.split(':')
|
||
success_parts = success_time.split(':')
|
||
|
||
loading_total = float(loading_parts[0])*3600 + float(loading_parts[1])*60 + float(loading_parts[2])
|
||
success_total = float(success_parts[0])*3600 + float(success_parts[1])*60 + float(success_parts[2])
|
||
|
||
duration_ms = (success_total - loading_total) * 1000
|
||
|
||
if 0 < duration_ms < 60000: # 0-60초 범위만 유효 (모델 다운로드 시간 고려)
|
||
model_name = None
|
||
if "simple lama model loaded successfully" in lines[j].lower():
|
||
model_name = "Simple LAMA"
|
||
elif "migan onnx model loaded successfully" in lines[j].lower():
|
||
model_name = "MIGAN"
|
||
elif "rembg model" in lines[j].lower() and "loaded successfully" in lines[j].lower():
|
||
model_name = "RemBG"
|
||
|
||
if model_name:
|
||
model_load_times[model_name].append(duration_ms)
|
||
break
|
||
except Exception as parse_error:
|
||
continue
|
||
|
||
# 통계 계산
|
||
stats = {}
|
||
for model_name, times in model_load_times.items():
|
||
if times:
|
||
stats[model_name] = {
|
||
"count": len(times),
|
||
"avg_ms": round(sum(times) / len(times), 1),
|
||
"min_ms": round(min(times), 1),
|
||
"max_ms": round(max(times), 1),
|
||
"recent_times": [round(t, 1) for t in times[-10:]] # 최근 10개
|
||
}
|
||
|
||
return {"stats": stats, "analysis_lines": len(lines)}
|
||
|
||
except Exception as e:
|
||
logger.error(f"성능 통계 조회 실패: {e}")
|
||
return {"stats": {}, "error": str(e)}
|
||
|
||
# 테스트용 엔드포인트
|
||
@api_router.get("/system-alerts")
|
||
async def get_system_alerts():
|
||
"""시스템 알림 반환"""
|
||
try:
|
||
alerts = []
|
||
|
||
# CPU 사용률 체크
|
||
cpu_percent = psutil.cpu_percent(interval=1)
|
||
if cpu_percent > 90:
|
||
alerts.append({
|
||
"level": "critical",
|
||
"message": f"CPU 사용률이 매우 높습니다: {cpu_percent:.1f}%",
|
||
"category": "system",
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
elif cpu_percent > 75:
|
||
alerts.append({
|
||
"level": "warning",
|
||
"message": f"CPU 사용률이 높습니다: {cpu_percent:.1f}%",
|
||
"category": "system",
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
|
||
# 메모리 사용률 체크
|
||
memory = psutil.virtual_memory()
|
||
if memory.percent > 90:
|
||
alerts.append({
|
||
"level": "critical",
|
||
"message": f"메모리 사용률이 매우 높습니다: {memory.percent:.1f}%",
|
||
"category": "system",
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
elif memory.percent > 80:
|
||
alerts.append({
|
||
"level": "warning",
|
||
"message": f"메모리 사용률이 높습니다: {memory.percent:.1f}%",
|
||
"category": "system",
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
|
||
# 디스크 사용률 체크
|
||
disk = psutil.disk_usage('/')
|
||
disk_percent = (disk.used / disk.total) * 100
|
||
if disk_percent > 90:
|
||
alerts.append({
|
||
"level": "critical",
|
||
"message": f"디스크 사용률이 매우 높습니다: {disk_percent:.1f}%",
|
||
"category": "system",
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
elif disk_percent > 80:
|
||
alerts.append({
|
||
"level": "warning",
|
||
"message": f"디스크 사용률이 높습니다: {disk_percent:.1f}%",
|
||
"category": "system",
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
|
||
# GPU 메모리 체크 (가능한 경우)
|
||
try:
|
||
gpu_info = gpu_monitor.get_gpu_memory_info()
|
||
if gpu_info.get("usage_percent", 0) > 95:
|
||
alerts.append({
|
||
"level": "critical",
|
||
"message": f"GPU 메모리 사용률이 매우 높습니다: {gpu_info['usage_percent']:.1f}%",
|
||
"category": "gpu",
|
||
"timestamp": datetime.now().isoformat()
|
||
})
|
||
except Exception:
|
||
pass
|
||
|
||
return {"alerts": alerts, "count": len(alerts)}
|
||
|
||
except Exception as e:
|
||
logger.error(f"시스템 알림 조회 실패: {e}")
|
||
return {"alerts": [], "error": str(e)}
|
||
|
||
@api_router.get("/test")
|
||
async def test_endpoint():
|
||
"""테스트용 엔드포인트입니다."""
|
||
try:
|
||
# status.json 파일 읽기 테스트
|
||
status = read_status_from_file()
|
||
|
||
# GPU 모니터 테스트
|
||
gpu_memory = gpu_monitor.get_gpu_memory_info()
|
||
system_memory = gpu_monitor.get_system_memory_info()
|
||
|
||
return {
|
||
"message": "테스트 성공",
|
||
"status_file": "읽기 성공" if status else "읽기 실패",
|
||
"gpu_memory": gpu_memory,
|
||
"system_memory": system_memory,
|
||
"timestamp": time.time()
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"테스트 엔드포인트 실패: {e}")
|
||
return {"error": str(e)}
|
||
|
||
@api_router.get("/test_data")
|
||
async def get_test_data():
|
||
"""테스트용 더미 데이터를 반환합니다."""
|
||
import random
|
||
return {
|
||
"timestamp": datetime.now().isoformat(),
|
||
"system_type": "Jetson Xavier",
|
||
"gpu": {
|
||
"total": 8.0,
|
||
"used": round(random.uniform(0.5, 2.0), 2),
|
||
"free": round(8.0 - random.uniform(0.5, 2.0), 2),
|
||
"usage_percent": round(random.uniform(5, 25), 1),
|
||
"utilization": round(random.uniform(0, 15), 1),
|
||
"temperature": round(random.uniform(35, 45), 1),
|
||
"clock_speed": random.randint(1100, 1300)
|
||
},
|
||
"system_memory": {
|
||
"total": 30.26,
|
||
"used": round(random.uniform(10, 15), 2),
|
||
"free": round(random.uniform(15, 20), 2),
|
||
"usage_percent": round(random.uniform(35, 50), 1)
|
||
},
|
||
"system_performance": {
|
||
"cpu_percent": round(random.uniform(5, 20), 1),
|
||
"cpu_count": 8,
|
||
"cpu_freq": {"current": 2266, "min": 1190, "max": 2265},
|
||
"load_avg": [round(random.uniform(0.1, 1.0), 2), round(random.uniform(0.1, 1.0), 2), round(random.uniform(0.1, 1.0), 2)],
|
||
"disk_io": {"read_mb": random.randint(10, 100), "write_mb": random.randint(5, 50), "read_count": random.randint(100, 1000), "write_count": random.randint(50, 500)},
|
||
"net_io": {"sent_mb": random.randint(1, 10), "recv_mb": random.randint(1, 10), "sent_packets": random.randint(100, 1000), "recv_packets": random.randint(100, 1000)},
|
||
"process_count": random.randint(300, 400)
|
||
},
|
||
"workers": {
|
||
"total_workers": 2,
|
||
"queue_size": random.randint(0, 5),
|
||
"workers_by_status": {
|
||
"idle": [{"id": "worker_1", "task_count": random.randint(10, 50)}],
|
||
"busy": [{"id": "worker_2", "current_task": "inpainting", "task_count": random.randint(5, 30)}] if random.random() > 0.5 else [],
|
||
"starting": [],
|
||
"stopping": [],
|
||
"error": []
|
||
},
|
||
"running": True
|
||
},
|
||
"sessions": {
|
||
"simple_lama": {"total": 2, "in_use": random.randint(0, 2), "available": 2 - random.randint(0, 2)},
|
||
"migan": {"total": 2, "in_use": random.randint(0, 2), "available": 2 - random.randint(0, 2)},
|
||
"rembg": {"total": 1, "in_use": random.randint(0, 1), "available": 1 - random.randint(0, 1)}
|
||
},
|
||
"api_stats": {
|
||
"total_requests": random.randint(100, 500),
|
||
"successful_requests": random.randint(90, 480),
|
||
"failed_requests": random.randint(0, 10),
|
||
"response_times": [round(random.uniform(0.1, 2.0), 2) for _ in range(10)],
|
||
"success_rate": round(random.uniform(85, 98), 1),
|
||
"avg_response_time": round(random.uniform(0.5, 1.5), 2),
|
||
"errors": []
|
||
},
|
||
"alerts": ["정보: 모니터링 데이터를 수집 중입니다..."] if random.random() > 0.7 else []
|
||
}
|
||
|
||
|
||
@api_router.get("/history")
|
||
async def get_history():
|
||
"""데이터 히스토리를 반환합니다."""
|
||
return {
|
||
"history": monitoring_data.get_history(),
|
||
"statistics": monitoring_data.get_statistics()
|
||
}
|
||
|
||
@api_router.get("/worker-status")
|
||
def get_worker_status_api():
|
||
"""워커 상태를 반환합니다."""
|
||
status = read_status_from_file()
|
||
return status.get("worker_status", {})
|
||
|
||
@api_router.get("/session-status")
|
||
def get_session_status_api():
|
||
"""세션 풀 상태를 반환합니다."""
|
||
status = read_status_from_file()
|
||
return status.get("session_status", {})
|
||
|
||
|
||
# FastAPI 앱에 라우터 포함
|
||
monitor_app.include_router(api_router, prefix="/api")
|
||
|
||
# WebSocket 핸들러
|
||
@monitor_app.websocket("/ws")
|
||
async def websocket_endpoint(websocket: WebSocket):
|
||
"""WebSocket 연결을 처리합니다."""
|
||
await websocket.accept()
|
||
connected_clients.append(websocket)
|
||
logger.info(f"WebSocket 클라이언트 연결됨: {websocket.client}")
|
||
|
||
try:
|
||
while True:
|
||
# 주기적으로 데이터 전송
|
||
data = await monitoring_data.collect_data()
|
||
|
||
# heartbeat 메시지 추가
|
||
data['heartbeat'] = time.time()
|
||
data['server_status'] = 'running'
|
||
|
||
try:
|
||
await websocket.send_json(data)
|
||
except (websockets.exceptions.ConnectionClosedOK,
|
||
websockets.exceptions.ConnectionClosedError,
|
||
RuntimeError) as e:
|
||
logger.info(f"WebSocket 연결이 끊어짐: {e}")
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"데이터 전송 오류: {e}")
|
||
break
|
||
|
||
await asyncio.sleep(2) # 2초마다 업데이트
|
||
|
||
except WebSocketDisconnect:
|
||
logger.info("클라이언트가 연결을 끊음")
|
||
except Exception as e:
|
||
logger.error(f"WebSocket 오류: {e}")
|
||
finally:
|
||
# 연결된 클라이언트 목록에서 제거
|
||
if websocket in connected_clients:
|
||
connected_clients.remove(websocket)
|
||
logger.info(f"WebSocket 클라이언트 연결 해제됨: {websocket.client}")
|
||
|
||
|
||
async def broadcast_data():
|
||
"""연결된 모든 클라이언트에게 데이터를 브로드캐스트합니다."""
|
||
while True:
|
||
try:
|
||
if connected_clients:
|
||
data = await monitoring_data.collect_data() # WebSocket 연결이 없으므로 None 전달
|
||
message = json.dumps(data, ensure_ascii=False)
|
||
|
||
# 연결이 끊어진 클라이언트 제거
|
||
disconnected = []
|
||
for client in connected_clients:
|
||
try:
|
||
await client.send_text(message)
|
||
except (websockets.exceptions.ConnectionClosedOK,
|
||
websockets.exceptions.ConnectionClosedError,
|
||
RuntimeError,
|
||
Exception) as e:
|
||
logger.debug(f"브로드캐스트 중 클라이언트 연결 끊어짐: {e}")
|
||
disconnected.append(client)
|
||
|
||
for client in disconnected:
|
||
connected_clients.remove(client)
|
||
|
||
await asyncio.sleep(2) # 2초마다 업데이트
|
||
|
||
except Exception as e:
|
||
logger.error(f"브로드캐스트 오류: {e}")
|
||
await asyncio.sleep(5)
|
||
|
||
|
||
# --- 서버 감시 및 자동 재시작 ---
|
||
HEALTH_CHECK_INTERVAL = 30 # 30초마다 확인
|
||
RESTART_COOLDOWN = 180 # 재시작 후 3분 대기
|
||
last_restart_time = 0
|
||
|
||
async def health_check_and_restart():
|
||
"""메인 서버의 상태를 주기적으로 확인하고, 다운 시 재시작합니다."""
|
||
global last_restart_time
|
||
logger.info("🩺 메인 서버 상태 감시 백그라운드 작업 시작...")
|
||
|
||
while True:
|
||
await asyncio.sleep(HEALTH_CHECK_INTERVAL)
|
||
|
||
try:
|
||
health_url = f"http://{settings.HOST}:{settings.PORT}/api/v1/health"
|
||
response = await asyncio.to_thread(requests.get, health_url, timeout=10)
|
||
|
||
if response.status_code == 200:
|
||
logger.debug(f"✅ 메인 서버 정상 응답 (상태 코드: {response.status_code})")
|
||
continue
|
||
else:
|
||
logger.warning(f"메인 서버 비정상 응답 (상태 코드: {response.status_code})")
|
||
|
||
except requests.RequestException as e:
|
||
logger.error(f"❌ 메인 서버 연결 실패: {e}")
|
||
|
||
# --- 서버 다운 감지 및 재시작 로직 ---
|
||
current_time = time.time()
|
||
if current_time - last_restart_time < RESTART_COOLDOWN:
|
||
logger.warning(f"재시작 대기 시간({RESTART_COOLDOWN}초)이 지나지 않아 재시작을 건너뜁니다.")
|
||
continue
|
||
|
||
logger.info("메인 서버 다운 감지. 재시작 절차를 시작합니다.")
|
||
last_restart_time = current_time
|
||
|
||
# 1. Discord 알림 발송
|
||
error_message = f"🚨 메인 서버(http://{settings.HOST}:{settings.PORT})가 응답하지 않습니다. 자동 재시작을 시도합니다."
|
||
send_discord_notification(error_message, level="error")
|
||
|
||
# 2. 서버 재시작 스크립트 실행
|
||
try:
|
||
script_path = os.path.join(settings.PROJECT_ROOT, "scripts", "start_server.sh")
|
||
logger.info(f"'{script_path}' 스크립트를 실행하여 서버를 재시작합니다.")
|
||
|
||
# 비동기로 서브프로세스 실행
|
||
process = await asyncio.create_subprocess_shell(
|
||
f"bash {script_path}",
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE
|
||
)
|
||
stdout, stderr = await process.communicate()
|
||
|
||
if process.returncode == 0:
|
||
success_message = "✅ 메인 서버 재시작 스크립트가 성공적으로 실행되었습니다."
|
||
logger.info(success_message)
|
||
send_discord_notification(success_message, level="success")
|
||
else:
|
||
error_log = stderr.decode(errors='ignore')
|
||
fail_message = f"❌ 서버 재시작 스크립트 실행 실패 (코드: {process.returncode})\n```\n{error_log}\n```"
|
||
logger.error(fail_message)
|
||
send_discord_notification(fail_message, level="error")
|
||
|
||
except Exception as e:
|
||
restart_fail_message = f"🔥 서버 재시작 중 치명적인 오류 발생: {e}"
|
||
logger.critical(restart_fail_message, exc_info=True)
|
||
send_discord_notification(restart_fail_message, level="error")
|
||
|
||
|
||
@monitor_app.on_event("startup")
|
||
async def start_monitoring():
|
||
"""모니터링 시작"""
|
||
logger.info("모니터링 대시보드 시작")
|
||
|
||
# Jetson 최적화 (시작 시)
|
||
if settings.IS_JETSON:
|
||
logger.info("Jetson Xavier 모드로 모니터링 시작")
|
||
gpu_monitor.optimize_for_jetson()
|
||
|
||
asyncio.create_task(broadcast_data())
|
||
asyncio.create_task(health_check_and_restart())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 로깅 설정
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||
)
|
||
|
||
# 모니터링 서버 실행
|
||
uvicorn.run(
|
||
"app.monitoring.dashboard:monitor_app",
|
||
host="0.0.0.0",
|
||
port=settings.MONITORING_PORT,
|
||
log_level="info"
|
||
)
|