yangtb24 commited on
Commit
34d7aaa
·
verified ·
1 Parent(s): d739a56

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +418 -0
app.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template_string
2
+ import requests
3
+ import threading
4
+ import time
5
+ from collections import defaultdict
6
+
7
+ app = Flask(__name__)
8
+
9
+ lightModeStyle = """
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+ body {
16
+ font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
17
+ background: #f0f2f5;
18
+ color: #333;
19
+ padding: 20px;
20
+ min-height: 100vh;
21
+ }
22
+ .container {
23
+ max-width: 1400px;
24
+ margin: 0 auto;
25
+ animation: fadeIn 0.5s ease;
26
+ padding: 0 20px;
27
+ }
28
+ .overview {
29
+ background: #fff;
30
+ border-radius: 15px;
31
+ padding: 25px;
32
+ margin-bottom: 30px;
33
+ border: 1px solid #dfe1e6;
34
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
35
+ }
36
+ .overview-title {
37
+ font-size: 20px;
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 10px;
41
+ margin-bottom: 20px;
42
+ color: #2e86de;
43
+ font-weight: 600;
44
+ }
45
+ .overview-title i {
46
+ margin-right: 8px;
47
+ }
48
+ #summary {
49
+ display: grid;
50
+ grid-template-columns: repeat(5, 1fr);
51
+ gap: 15px;
52
+ }
53
+ #summary div {
54
+ background: #f9fafb;
55
+ padding: 15px;
56
+ border-radius: 8px;
57
+ border: 1px solid #dfe1e6;
58
+ transition: background-color 0.2s ease;
59
+ }
60
+ #summary div:hover {
61
+ background-color: #f0f2f5;
62
+ }
63
+ #summary div {
64
+ font-size: 14px;
65
+ color: #555;
66
+ }
67
+ #summary span {
68
+ display: block;
69
+ font-size: 24px;
70
+ font-weight: bold;
71
+ margin-top: 5px;
72
+ color: #333;
73
+ }
74
+ .stats-container {
75
+ display: grid;
76
+ grid-template-columns: repeat(2, 1fr);
77
+ gap: 20px;
78
+ margin-top: 20px;
79
+ }
80
+ .server-card {
81
+ background: #fff;
82
+ border-radius: 10px;
83
+ padding: 20px;
84
+ border: 1px solid #dfe1e6;
85
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
86
+ height: auto;
87
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
88
+ }
89
+ .server-card:hover {
90
+ transform: translateY(-3px);
91
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
92
+ }
93
+ .server-header {
94
+ display: flex;
95
+ justify-content: space-between;
96
+ align-items: center;
97
+ margin-bottom: 15px;
98
+ font-size: 16px;
99
+ color: #444;
100
+ }
101
+ .server-name {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 10px;
105
+ }
106
+ .server-flag {
107
+ width: 20px;
108
+ height: 20px;
109
+ border-radius: 4px;
110
+ }
111
+ .metric-grid {
112
+ display: grid;
113
+ grid-template-columns: repeat(2, 1fr);
114
+ gap: 15px;
115
+ margin-top: 15px;
116
+ }
117
+ .metric-item {
118
+ background: #f9fafb;
119
+ padding: 12px;
120
+ border-radius: 8px;
121
+ border: 1px solid #dfe1e6;
122
+ transition: background-color 0.2s ease;
123
+ }
124
+ .metric-item:hover {
125
+ background-color: #f0f2f5;
126
+ }
127
+ .metric-label {
128
+ color: #777;
129
+ font-size: 13px;
130
+ margin-bottom: 5px;
131
+ }
132
+ .metric-value {
133
+ font-size: 16px;
134
+ font-weight: 500;
135
+ color: #333;
136
+ }
137
+ .status-dot {
138
+ display: inline-block;
139
+ border-radius: 50%;
140
+ animation: pulse 2s infinite;
141
+ width: 12px;
142
+ height: 12px;
143
+ }
144
+ .status-online {
145
+ background-color: #2ecc71;
146
+ color: #2ecc71;
147
+ box-shadow: 0 0 5px rgba(46, 204, 113, 0.4);
148
+ }
149
+ .status-offline {
150
+ background-color: #e74c3c;
151
+ color: #e74c3c;
152
+ box-shadow: 0 0 5px rgba(231, 76, 60, 0.4);
153
+ }
154
+ @keyframes fadeIn {
155
+ from { opacity: 0; transform: translateY(20px); }
156
+ to { opacity: 1; transform: translateY(0); }
157
+ }
158
+ @keyframes pulse {
159
+ 0% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.4); }
160
+ 70% { box-shadow: 0 0 0 10px rgba(46, 204, 113, 0); }
161
+ 100% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0); }
162
+ }
163
+ @media (max-width: 768px) {
164
+ #summary {
165
+ grid-template-columns: 1fr;
166
+ }
167
+ .stats-container {
168
+ grid-template-columns: 1fr;
169
+ }
170
+ .metric-grid {
171
+ grid-template-columns: 1fr;
172
+ }
173
+ }
174
+ .progress-bar-container {
175
+ width: 100%;
176
+ background-color: #ddd;
177
+ border-radius: 4px;
178
+ margin-top: 4px;
179
+ }
180
+ .progress-bar {
181
+ height: 8px;
182
+ border-radius: 4px;
183
+ background-color: #2e86de;
184
+ width: 0%;
185
+ }
186
+ .cpu-progress-bar {
187
+ height: 8px;
188
+ border-radius: 4px;
189
+ background-color: #2ecc71;
190
+ width: 0%;
191
+ }
192
+ .memory-progress-bar{
193
+ height: 8px;
194
+ border-radius: 4px;
195
+ background-color: #e67e22;
196
+ width: 0%;
197
+ }
198
+ """
199
+
200
+ htmlTemplate = f"""
201
+ <!DOCTYPE html>
202
+ <html lang="zh">
203
+ <head>
204
+ <meta charset="UTF-8">
205
+ <title>HF Space Monitor</title>
206
+ <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>">
207
+ <style>{lightModeStyle}</style>
208
+ </head>
209
+ <body>
210
+ <div class="container">
211
+ <div class="overview">
212
+ <div class="overview-title"><i class="fas fa-chart-line"></i>系统概览</div>
213
+ <div id="summary">
214
+ <div>总实例数: <span id="totalServers">0</span></div>
215
+ <div>在线实例: <span id="onlineServers">0</span></div>
216
+ <div>离线实例: <span id="offlineServers">0</span></div>
217
+ <div>总上传: <span id="totalUpload">0 B/s</span></div>
218
+ <div>总下载: <span id="totalDownload">0 B/s</span></div>
219
+ </div>
220
+ </div>
221
+ <div id="servers" class="stats-container">
222
+ <!-- 服务器卡片将在这里动态生成 -->
223
+ </div>
224
+ </div>
225
+ <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>
226
+
227
+ <script>
228
+ const username = 'yangtb24';
229
+ let serversData = {{}};
230
+
231
+ function updateUI() {{
232
+ let totalServers = 0;
233
+ let onlineServers = 0;
234
+ let offlineServers = 0;
235
+ let totalUpload = 0;
236
+ let totalDownload = 0;
237
+
238
+ const serversContainer = document.getElementById('servers');
239
+ serversContainer.innerHTML = '';
240
+
241
+ for (const spaceId in serversData) {{
242
+ for (const serverId in serversData[spaceId]) {{
243
+ const server = serversData[spaceId][serverId];
244
+ totalServers++;
245
+ const isOnline = server.status === 'online';
246
+ if (isOnline) {{
247
+ onlineServers++;
248
+ totalUpload += server.uploadBps;
249
+ totalDownload += server.downloadBps;
250
+
251
+ }} else {{
252
+ offlineServers++;
253
+ }}
254
+
255
+ const card = document.createElement('div');
256
+ card.id = `server-${serverId}`;
257
+ card.className = 'server-card';
258
+ card.innerHTML = `
259
+ <div class="server-header">
260
+ <div class="server-name">
261
+ <div class="status-dot status-${{isOnline ? 'online' : 'offline'}}"></div>
262
+ <svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
263
+ <path d="M21 3H3C1.9 3 1 3.9 1 5v3c0 1.1.9 2 2 2h18c1.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"/>
264
+ </svg>
265
+ <div>${{serverId}} (${{username}}/${{spaceId}})</div>
266
+ </div>
267
+ </div>
268
+ <div class="metric-grid">
269
+ <div class="metric-item">
270
+ <div class="metric-label">CPU</div>
271
+ <div class="progress-bar-container">
272
+ <div class="cpu-progress-bar" style="width: ${{server.cpuUsage}}%"></div>
273
+ </div>
274
+ <div class="metric-value cpu-usage">${{server.cpuUsage.toFixed(2)}}%</div>
275
+ </div>
276
+ <div class="metric-item">
277
+ <div class="metric-label">内存</div>
278
+ <div class="progress-bar-container">
279
+ <div class="memory-progress-bar" style="width: ${{server.memoryUsage}}%"></div>
280
+ </div>
281
+ <div class="metric-value memory-usage">${{server.memoryUsage.toFixed(2)}}%</div>
282
+ </div>
283
+ <div class="metric-item">
284
+ <div class="metric-label">上传</div>
285
+ <div class="metric-value upload">${{formatBytes(server.uploadBps)}}/s</div>
286
+ </div>
287
+ <div class="metric-item">
288
+ <div class="metric-label">下载</div>
289
+ <div class="metric-value download">${{formatBytes(server.downloadBps)}}/s</div>
290
+ </div>
291
+ </div>
292
+ `;
293
+ serversContainer.appendChild(card);
294
+ }}
295
+ }}
296
+ document.getElementById('totalServers').textContent = totalServers;
297
+ document.getElementById('onlineServers').textContent = onlineServers;
298
+ document.getElementById('offlineServers').textContent = offlineServers;
299
+ document.getElementById('totalUpload').textContent = `${{formatBytes(totalUpload)}}/s`;
300
+ document.getElementById('totalDownload').textContent = `${{formatBytes(totalDownload)}}/s`;
301
+ }}
302
+
303
+ function formatBytes(bytes) {{
304
+ if (bytes === 0) return '0 B';
305
+ const k = 1024;
306
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
307
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
308
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
309
+ }}
310
+
311
+ updateUI();
312
+
313
+ fetch('/update_data')
314
+ .then(response => response.json())
315
+ .then(data => {{
316
+ serversData = data;
317
+ updateUI();
318
+
319
+ setInterval(() => {{
320
+ fetch('/update_data')
321
+ .then(response => response.json())
322
+ .then(data => {{
323
+ serversData = data;
324
+ updateUI();
325
+ }});
326
+ }}, 2000);
327
+ }});
328
+ </script>
329
+ </body>
330
+ </html>
331
+ """
332
+
333
+ USERNAME = 'yangtb24'
334
+ servers_data = defaultdict(lambda: defaultdict(dict))
335
+ last_seen = defaultdict(lambda: defaultdict(lambda: 0))
336
+
337
+ def fetch_instances(username):
338
+ try:
339
+ response = requests.get(f"https://huggingface.co/api/spaces?author={username}")
340
+ response.raise_for_status()
341
+ user_instances = response.json()
342
+ return [instance['id'].split('/')[1] for instance in user_instances]
343
+ except requests.RequestException as e:
344
+ print(f"Error fetching instances: {e}")
345
+ return []
346
+
347
+ def fetch_metrics(username, instance_id):
348
+ try:
349
+ response = requests.get(f"https://api.hf.space/v1/{username}/{instance_id}/live-metrics/sse", stream=True)
350
+ response.raise_for_status()
351
+
352
+ for line in response.iter_lines():
353
+ if line:
354
+ decoded_line = line.decode('utf-8')
355
+ if decoded_line.startswith("event: metric"):
356
+ try:
357
+ data_part = decoded_line.split("data: ", 1)[1]
358
+ data = eval(data_part) # Use eval (safer than json.loads for this specific HF API)
359
+
360
+ server_id = data['replica']
361
+ cpu_usage = data['cpu_usage_pct']
362
+ memory_usage = (data['memory_used_bytes'] / data['memory_total_bytes']) * 100
363
+ upload_bps = data['tx_bps']
364
+ download_bps = data['rx_bps']
365
+
366
+ servers_data[instance_id][server_id] = {
367
+ 'cpuUsage': cpu_usage,
368
+ 'memoryUsage': memory_usage,
369
+ 'uploadBps': upload_bps,
370
+ 'downloadBps': download_bps,
371
+ 'status': 'online'
372
+ }
373
+ last_seen[instance_id][server_id] = time.time()
374
+
375
+ except (IndexError, KeyError, SyntaxError) as e:
376
+ print(f"Error parsing metric data: {e}, data: {decoded_line}")
377
+
378
+ except requests.RequestException as e:
379
+ print(f"Error fetching metrics for {instance_id}: {e}")
380
+ for server_id in list(servers_data[instance_id].keys()):
381
+ servers_data[instance_id][server_id]['status'] = 'offline'
382
+
383
+ def check_offline_servers():
384
+ while True:
385
+ now = time.time()
386
+ for instance_id in list(last_seen.keys()):
387
+ for server_id in list(last_seen[instance_id].keys()):
388
+ if now - last_seen[instance_id][server_id] > 10:
389
+ if instance_id in servers_data and server_id in servers_data[instance_id]:
390
+ servers_data[instance_id][server_id]['status'] = 'offline'
391
+ time.sleep(2)
392
+
393
+ def start_monitoring(username):
394
+ instances = fetch_instances(username)
395
+ if not instances:
396
+ print("No instances found.")
397
+ return
398
+
399
+ threads = []
400
+ for instance_id in instances:
401
+ thread = threading.Thread(target=fetch_metrics, args=(username, instance_id), daemon=True)
402
+ threads.append(thread)
403
+ thread.start()
404
+
405
+ offline_check_thread = threading.Thread(target=check_offline_servers, daemon=True)
406
+ offline_check_thread.start()
407
+
408
+ @app.route('/')
409
+ def index():
410
+ return render_template_string(htmlTemplate)
411
+
412
+ @app.route('/update_data')
413
+ def update_data():
414
+ return servers_data
415
+
416
+ if __name__ == '__main__':
417
+ start_monitoring(USERNAME)
418
+ app.run(debug=False, host="0.0.0.0", port=7860)