Monitor / app.py
yangtb24's picture
Update app.py
a188021 verified
from flask import Flask, render_template_string, jsonify, Response
import requests
import os
app = Flask(__name__)
lightModeStyle = """
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
background: #f0f2f5;
color: #333;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
animation: fadeIn 0.5s ease;
padding: 0 20px;
}
.overview {
background: #fff;
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
border: 1px solid #dfe1e6;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.overview-title {
font-size: 20px;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
color: #2e86de;
font-weight: 600;
}
.overview-title i {
margin-right: 8px;
}
#summary {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 15px;
}
#summary div {
background: #f9fafb;
padding: 15px;
border-radius: 8px;
border: 1px solid #dfe1e6;
transition: background-color 0.2s ease;
}
#summary div:hover {
background-color: #f0f2f5;
}
#summary div {
font-size: 14px;
color: #555;
}
#summary span {
display: block;
font-size: 24px;
font-weight: bold;
margin-top: 5px;
color: #333;
}
.stats-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-top: 20px;
}
.server-card {
background: #fff;
border-radius: 10px;
padding: 20px;
border: 1px solid #dfe1e6;
transition: transform 0.2s ease, box-shadow 0.2s ease;
height: auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.server-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.server-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
font-size: 16px;
color: #444;
}
.server-name {
display: flex;
align-items: center;
gap: 10px;
}
.server-flag {
width: 20px;
height: 20px;
border-radius: 4px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-top: 15px;
}
.metric-item {
background: #f9fafb;
padding: 12px;
border-radius: 8px;
border: 1px solid #dfe1e6;
transition: background-color 0.2s ease;
}
.metric-item:hover {
background-color: #f0f2f5;
}
.metric-label {
color: #777;
font-size: 13px;
margin-bottom: 5px;
}
.metric-value {
font-size: 16px;
font-weight: 500;
color: #333;
}
.status-dot {
display: inline-block;
border-radius: 50%;
animation: pulse 2s infinite;
width: 12px;
height: 12px;
}
.status-online {
background-color: #2ecc71;
color: #2ecc71;
box-shadow: 0 0 5px rgba(46, 204, 113, 0.4);
}
.status-offline {
background-color: #e74c3c;
color: #e74c3c;
box-shadow: 0 0 5px rgba(231, 76, 60, 0.4);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(46, 204, 113, 0); }
100% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0); }
}
@media (max-width: 768px) {
#summary {
grid-template-columns: 1fr;
}
.stats-container {
grid-template-columns: 1fr;
}
.metric-grid {
grid-template-columns: 1fr;
}
}
.progress-bar-container {
width: 100%;
background-color: #ddd;
border-radius: 4px;
margin-top: 4px;
}
.progress-bar {
height: 8px;
border-radius: 4px;
background-color: #2e86de;
width: 0%;
}
.cpu-progress-bar {
height: 8px;
border-radius: 4px;
background-color: #2ecc71;
width: 0%;
}
.memory-progress-bar{
height: 8px;
border-radius: 4px;
background-color: #e67e22;
width: 0%;
}
"""
htmlTemplate = f"""
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>HF Space Monitor</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚀</text></svg>">
<style>{lightModeStyle}</style>
</head>
<body>
<div class="container">
<div class="overview">
<div class="overview-title"><i class="fas fa-chart-line"></i>系统概览</div>
<div id="summary">
<div>总实例数: <span id="totalServers">0</span></div>
<div>在线实例: <span id="onlineServers">0</span></div>
<div>离线实例: <span id="offlineServers">0</span></div>
<div>总上传: <span id="totalUpload">0 B/s</span></div>
<div>总下载: <span id="totalDownload">0 B/s</span></div>
</div>
</div>
<div id="servers" class="stats-container">
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js" integrity="sha512-yFjZbTYRCJodnuyGlsKamNE/LlEaEA/3uWCGാരി7eIq7jWqVl3J8jL/kof/tfu9Xqzh/y/VM5sJd/tq5iEew==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
const username = '{{username}}';
async function fetchInstances() {{
try {{
const response = await fetch(`/instances`);
const userInstances = await response.json();
return userInstances;
}} catch (error) {{
console.error("获取实例列表失败:", error);
return [];
}}
}}
class MetricsManager {{
constructor() {{
this.eventSources = new Map();
this.servers = new Map();
this.instanceOwners = new Map();
this.spaceIds = new Map();
}}
async connect(instanceId, username) {{
if (this.eventSources.has(instanceId)) return;
try {{
const eventSource = new EventSource(`/metrics/${{username}}/${{instanceId}}`);
this.spaceIds.set(instanceId, instanceId);
this.instanceOwners.set(instanceId, username);
eventSource.addEventListener("metric", (event) => {{
try {{
const data = JSON.parse(event.data);
// console.log("Received data:", data); // Debugging line
updateServerCard(data, instanceId);
}} catch (error) {{
console.error(`解析数据失败 (${{instanceId}}):`, error);
}}
}});
eventSource.onerror = (error) => {{
console.error(`EventSource 错误 (${{instanceId}}):`, error);
eventSource.close();
this.eventSources.delete(instanceId); // Remove on error
}};
this.eventSources.set(instanceId, eventSource);
}} catch (error) {{
console.error(`连接失败 (${{username}}/${{instanceId}}):`, error);
}}
}}
disconnectAll() {{
this.eventSources.forEach(es => es.close());
this.eventSources.clear();
}}
}}
const metricsManager = new MetricsManager();
const servers = new Map();
async function initialize() {{
const instances = await fetchInstances();
instances.forEach(instance => {{
metricsManager.connect(instance.id, instance.owner);
}});
}}
function updateServerCard(data, spaceId) {{
const serverId = data.replica;
const serverElement = document.getElementById(`server-${{serverId}}`);
const owner = metricsManager.instanceOwners.get(spaceId);
if (!serverElement) {{
const card = document.createElement('div');
card.id = `server-${{serverId}}`;
card.className = 'server-card';
card.innerHTML = `
<div class="server-header">
<div class="server-name">
<div class="status-dot status-online"></div>
<svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 3H3C1.9 3 1 3.9 1 5v3c0 1.1.9 2 2 10h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-1 5H4V6h16v2zm1 4H3c-1.1 0-2 .9-2 2v3c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2v-3c0-1.1-.9-2-2-2zm-1 5H4v-2h16v2zm1 4H3c-1.1 0-2 .9-2 2v3c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2v-3c0-1.1-.9-2-2-2zm-1 5H4v-2h16v2z"/>
</svg>
<div>${{serverId}} (${{owner}}/${{spaceId}})</div>
</div>
</div>
<div class="metric-grid">
<div class="metric-item">
<div class="metric-label">CPU</div>
<div class="progress-bar-container">
<div class="cpu-progress-bar"></div>
</div>
<div class="metric-value cpu-usage">0%</div>
</div>
<div class="metric-item">
<div class="metric-label">内存</div>
<div class="progress-bar-container">
<div class="memory-progress-bar"></div>
</div>
<div class="metric-value memory-usage">0%</div>
</div>
<div class="metric-item">
<div class="metric-label">上传</div>
<div class="metric-value upload">0 KB/s</div>
</div>
<div class="metric-item">
<div class="metric-label">下载</div>
<div class="metric-value download">0 KB/s</div>
</div>
</div>
`;
document.getElementById('servers').appendChild(card);
}}
const card = document.getElementById(`server-${{serverId}}`);
const cpuUsage = data.cpu_usage_pct;
const memoryUsage = (data.memory_used_bytes / data.memory_total_bytes) * 100;
const uploadBps = data.tx_bps;
const downloadBps = data.rx_bps;
card.querySelector('.cpu-usage').textContent = `${{cpuUsage.toFixed(2)}}%`;
card.querySelector('.cpu-progress-bar').style.width = `${{cpuUsage}}%`;
card.querySelector('.memory-usage').textContent = `${{memoryUsage.toFixed(2)}}%`;
card.querySelector('.memory-progress-bar').style.width = `${{memoryUsage}}%`;
card.querySelector('.upload').textContent = `${{formatBytes(uploadBps)}}/s`;
card.querySelector('.download').textContent = `${{formatBytes(downloadBps)}}/s`;
servers.set(serverId, Date.now());
updateSummary();
}}
function updateSummary() {{
const now = Date.now();
let online = 0;
let offline = 0;
let totalUpload = 0;
let totalDownload = 0;
servers.forEach((lastSeen, serverId) => {{
const isOnline = (now - lastSeen) < 10000;
const serverCard = document.getElementById(`server-${{serverId}}`);
if (serverCard) {{
const statusDot = serverCard.querySelector('.status-dot');
statusDot.className = `status-dot status-${{isOnline ? 'online' : 'offline'}}`;
if (isOnline) {{
const uploadText = serverCard.querySelector('.upload').textContent;
const downloadText = serverCard.querySelector('.download').textContent;
totalUpload += parseFloat(uploadText) || 0;
totalDownload += parseFloat(downloadText) || 0;
}}
}}
isOnline ? online++ : offline++;
}});
document.getElementById('totalServers').textContent = servers.size;
document.getElementById('onlineServers').textContent = online;
document.getElementById('offlineServers').textContent = offline;
document.getElementById('totalUpload').textContent = `${{formatBytes(totalUpload)}}/s`;
document.getElementById('totalDownload').textContent = `${{formatBytes(totalDownload)}}/s`;
}}
function formatBytes(bytes) {{
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}}
initialize(); // Initial load
setInterval(updateSummary, 2000);
setInterval(async () => {{
metricsManager.disconnectAll();
await initialize();
}}, 300000);
</script>
</body>
</html>
"""
USERNAME = os.environ.get("USERNAME", "yangtb24")
def fetch_instances(username):
try:
response = requests.get(f"https://huggingface.co/api/spaces?author={username}")
response.raise_for_status()
user_instances = response.json()
return [{"id": instance["id"].split('/')[1], "owner": username} for instance in user_instances]
except requests.exceptions.RequestException as e:
print(f"Error fetching instances: {e}")
return []
@app.route('/')
def index():
return render_template_string(htmlTemplate, username=USERNAME)
@app.route('/instances')
def get_instances():
instances = fetch_instances(USERNAME)
return jsonify(instances)
@app.route('/metrics/<username>/<instance_id>')
def stream_metrics(username, instance_id):
url = f"https://api.hf.space/v1/{username}/{instance_id}/live-metrics/sse"
def generate():
try:
response = requests.get(url, stream=True, headers={"Accept": "text/event-stream"}, timeout=15)
response.raise_for_status()
buffer = ""
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
if chunk:
buffer += chunk
while "\n\n" in buffer:
event_data, buffer = buffer.split("\n\n", 1)
lines = event_data.split("\n")
event_type = "message"
data_lines = []
for line in lines:
if line.startswith("event:"):
event_type = line.split(":", 1)[1].strip()
elif line.startswith("data:"):
data_lines.append(line.split(":", 1)[1].strip())
if event_type == "metric":
yield f"event: {event_type}\ndata: {''.join(data_lines)}\n\n"
except requests.exceptions.RequestException as e:
print(f"Request Exception: {e}")
yield f"event: error\ndata: Connection error: {e}\\n\\n"
except Exception as e:
print(f"An error occurred: {e}")
yield f"event: error\ndata: An error occurred: {e}\\n\\n"
return Response(generate(), mimetype='text/event-stream')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=7860)