inpaintServer/app/monitoring/dashboard.py

2106 lines
90 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
워커 감시 대시보드
실시간으로 워커 상태, 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
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 < 10000: # 0-10초 범위만 유효
model_name = "Simple LAMA" # 기본값
if "simple_lama" in line.lower():
model_name = "Simple LAMA"
elif "migan" in line.lower():
model_name = "MIGAN"
elif "rembg" in line.lower():
model_name = "RemBG"
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)
@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())
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"
)