summaryrefslogtreecommitdiffstats
path: root/internal/metrics/dashboard.html
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-14 12:17:33 -0800
committerbndw <ben@bdw.to>2026-02-14 12:17:33 -0800
commit865b3da22881a1046c15e99bdd5fbc64aa374b73 (patch)
treeb82401b650780df42eebe778f9ae16512afab281 /internal/metrics/dashboard.html
parentea4f508f5ee91b370c6912cde26b1a432380d037 (diff)
feat: add metrics dashboard HTML page
Add a real-time metrics dashboard accessible at the metrics server root. The dashboard displays relay statistics in a human-readable format with auto-refresh every 5 seconds. Features: - Active connections and subscriptions - Request counts (total, success, errors) - Authentication stats (success/failure) - Rate limiting hits - Storage metrics (events, DB size, deletions) - Clean, dark-themed UI with gradient accents - Auto-refresh and live uptime counter The dashboard is embedded using go:embed and served at / while Prometheus metrics continue to be available at /metrics. Example: http://localhost:9090/ for dashboard http://localhost:9090/metrics for raw metrics
Diffstat (limited to 'internal/metrics/dashboard.html')
-rw-r--r--internal/metrics/dashboard.html314
1 files changed, 314 insertions, 0 deletions
diff --git a/internal/metrics/dashboard.html b/internal/metrics/dashboard.html
new file mode 100644
index 0000000..800b6df
--- /dev/null
+++ b/internal/metrics/dashboard.html
@@ -0,0 +1,314 @@
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Nostr Relay Metrics</title>
7 <style>
8 * { margin: 0; padding: 0; box-sizing: border-box; }
9 body {
10 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11 background: #0a0a0a;
12 color: #e0e0e0;
13 padding: 2rem;
14 line-height: 1.6;
15 }
16 .container { max-width: 1200px; margin: 0 auto; }
17 h1 {
18 font-size: 2rem;
19 margin-bottom: 0.5rem;
20 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
21 -webkit-background-clip: text;
22 -webkit-text-fill-color: transparent;
23 background-clip: text;
24 }
25 .subtitle { color: #888; margin-bottom: 2rem; font-size: 0.9rem; }
26 .grid {
27 display: grid;
28 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
29 gap: 1.5rem;
30 margin-bottom: 2rem;
31 }
32 .card {
33 background: #1a1a1a;
34 border: 1px solid #333;
35 border-radius: 8px;
36 padding: 1.5rem;
37 }
38 .card h2 {
39 font-size: 0.875rem;
40 text-transform: uppercase;
41 letter-spacing: 0.05em;
42 color: #888;
43 margin-bottom: 1rem;
44 }
45 .metric {
46 display: flex;
47 justify-content: space-between;
48 align-items: baseline;
49 margin-bottom: 0.75rem;
50 }
51 .metric-label { color: #aaa; font-size: 0.9rem; }
52 .metric-value {
53 font-size: 1.5rem;
54 font-weight: 600;
55 color: #fff;
56 }
57 .metric-value.large { font-size: 2.5rem; }
58 .metric-value.success { color: #10b981; }
59 .metric-value.warning { color: #f59e0b; }
60 .metric-value.error { color: #ef4444; }
61 .metric-unit { font-size: 0.875rem; color: #666; margin-left: 0.25rem; }
62 .status {
63 display: inline-block;
64 padding: 0.25rem 0.75rem;
65 border-radius: 12px;
66 font-size: 0.75rem;
67 font-weight: 600;
68 }
69 .status.online { background: #10b98120; color: #10b981; }
70 .status.offline { background: #ef444420; color: #ef4444; }
71 .refresh-info {
72 text-align: center;
73 color: #666;
74 font-size: 0.8rem;
75 margin-top: 2rem;
76 }
77 .error-msg {
78 background: #ef444420;
79 border: 1px solid #ef4444;
80 color: #ef4444;
81 padding: 1rem;
82 border-radius: 8px;
83 margin-bottom: 1rem;
84 }
85 </style>
86</head>
87<body>
88 <div class="container">
89 <h1>Nostr Relay Metrics</h1>
90 <p class="subtitle">Real-time relay statistics and performance metrics</p>
91
92 <div id="error"></div>
93
94 <div class="grid">
95 <div class="card">
96 <h2>Status</h2>
97 <div class="metric">
98 <span class="metric-label">Relay</span>
99 <span class="status online">Online</span>
100 </div>
101 <div class="metric">
102 <span class="metric-label">Uptime</span>
103 <span class="metric-value" id="uptime">--</span>
104 </div>
105 </div>
106
107 <div class="card">
108 <h2>Connections</h2>
109 <div class="metric">
110 <span class="metric-label">Active</span>
111 <span class="metric-value large success" id="active_connections">0</span>
112 </div>
113 <div class="metric">
114 <span class="metric-label">Total</span>
115 <span class="metric-value" id="total_connections">0</span>
116 </div>
117 </div>
118
119 <div class="card">
120 <h2>Subscriptions</h2>
121 <div class="metric">
122 <span class="metric-label">Active</span>
123 <span class="metric-value large" id="active_subscriptions">0</span>
124 </div>
125 </div>
126
127 <div class="card">
128 <h2>Requests</h2>
129 <div class="metric">
130 <span class="metric-label">Total</span>
131 <span class="metric-value" id="total_requests">0</span>
132 </div>
133 <div class="metric">
134 <span class="metric-label">Success</span>
135 <span class="metric-value success" id="success_requests">0</span>
136 </div>
137 <div class="metric">
138 <span class="metric-label">Errors</span>
139 <span class="metric-value error" id="error_requests">0</span>
140 </div>
141 </div>
142
143 <div class="card">
144 <h2>Authentication</h2>
145 <div class="metric">
146 <span class="metric-label">Success</span>
147 <span class="metric-value success" id="auth_success">0</span>
148 </div>
149 <div class="metric">
150 <span class="metric-label">Failed</span>
151 <span class="metric-value error" id="auth_failure">0</span>
152 </div>
153 </div>
154
155 <div class="card">
156 <h2>Rate Limiting</h2>
157 <div class="metric">
158 <span class="metric-label">Blocked</span>
159 <span class="metric-value warning" id="rate_limit_hits">0</span>
160 </div>
161 </div>
162
163 <div class="card">
164 <h2>Storage</h2>
165 <div class="metric">
166 <span class="metric-label">Events</span>
167 <span class="metric-value" id="events_total">0</span>
168 </div>
169 <div class="metric">
170 <span class="metric-label">DB Size</span>
171 <span class="metric-value" id="db_size">0<span class="metric-unit">MB</span></span>
172 </div>
173 <div class="metric">
174 <span class="metric-label">Deletions</span>
175 <span class="metric-value" id="event_deletions">0</span>
176 </div>
177 </div>
178
179 <div class="card">
180 <h2>Performance</h2>
181 <div class="metric">
182 <span class="metric-label">Avg Latency</span>
183 <span class="metric-value" id="avg_latency">0<span class="metric-unit">ms</span></span>
184 </div>
185 </div>
186 </div>
187
188 <p class="refresh-info">Auto-refreshing every 5 seconds</p>
189 </div>
190
191 <script>
192 let startTime = Date.now();
193
194 function parsePrometheusMetrics(text) {
195 const metrics = {};
196 const lines = text.split('\n');
197
198 for (const line of lines) {
199 if (line.startsWith('#') || !line.trim()) continue;
200
201 const match = line.match(/^([a-zA-Z_:][a-zA-Z0-9_:]*)\{?(.*?)\}?\s+(.+)$/);
202 if (!match) continue;
203
204 const [, name, labels, value] = match;
205 const labelObj = {};
206
207 if (labels) {
208 const labelMatches = labels.matchAll(/(\w+)="([^"]+)"/g);
209 for (const [, key, val] of labelMatches) {
210 labelObj[key] = val;
211 }
212 }
213
214 if (!metrics[name]) metrics[name] = [];
215 metrics[name].push({ labels: labelObj, value: parseFloat(value) });
216 }
217
218 return metrics;
219 }
220
221 function sumMetric(metrics, name) {
222 if (!metrics[name]) return 0;
223 return metrics[name].reduce((sum, m) => sum + m.value, 0);
224 }
225
226 function getMetricByLabel(metrics, name, labelKey, labelValue) {
227 if (!metrics[name]) return 0;
228 const metric = metrics[name].find(m => m.labels[labelKey] === labelValue);
229 return metric ? metric.value : 0;
230 }
231
232 function formatUptime(ms) {
233 const seconds = Math.floor(ms / 1000);
234 const minutes = Math.floor(seconds / 60);
235 const hours = Math.floor(minutes / 60);
236 const days = Math.floor(hours / 24);
237
238 if (days > 0) return `${days}d ${hours % 24}h`;
239 if (hours > 0) return `${hours}h ${minutes % 60}m`;
240 if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
241 return `${seconds}s`;
242 }
243
244 function formatBytes(bytes) {
245 if (bytes === 0) return '0';
246 const mb = bytes / (1024 * 1024);
247 return mb.toFixed(2);
248 }
249
250 async function updateMetrics() {
251 try {
252 const response = await fetch('/metrics');
253 if (!response.ok) throw new Error('Failed to fetch metrics');
254
255 const text = await response.text();
256 const metrics = parsePrometheusMetrics(text);
257
258 const prefix = Object.keys(metrics)[0]?.split('_')[0] || 'muxstr';
259
260 document.getElementById('active_connections').textContent =
261 sumMetric(metrics, `${prefix}_relay_active_connections`);
262
263 document.getElementById('total_connections').textContent =
264 sumMetric(metrics, `${prefix}_relay_connections_total`);
265
266 document.getElementById('active_subscriptions').textContent =
267 sumMetric(metrics, `${prefix}_relay_active_subscriptions`);
268
269 document.getElementById('total_requests').textContent =
270 sumMetric(metrics, `${prefix}_relay_requests_total`);
271
272 document.getElementById('success_requests').textContent =
273 getMetricByLabel(metrics, `${prefix}_relay_requests_total`, 'status', 'ok');
274
275 document.getElementById('error_requests').textContent =
276 getMetricByLabel(metrics, `${prefix}_relay_requests_total`, 'status', 'error');
277
278 document.getElementById('auth_success').textContent =
279 getMetricByLabel(metrics, `${prefix}_relay_auth_attempts_total`, 'result', 'success');
280
281 document.getElementById('auth_failure').textContent =
282 getMetricByLabel(metrics, `${prefix}_relay_auth_attempts_total`, 'result', 'failure');
283
284 document.getElementById('rate_limit_hits').textContent =
285 sumMetric(metrics, `${prefix}_relay_rate_limit_hits_total`);
286
287 document.getElementById('events_total').textContent =
288 sumMetric(metrics, `${prefix}_relay_events_total`);
289
290 const dbSize = sumMetric(metrics, `${prefix}_relay_db_size_bytes`);
291 document.getElementById('db_size').innerHTML =
292 `${formatBytes(dbSize)}<span class="metric-unit">MB</span>`;
293
294 document.getElementById('event_deletions').textContent =
295 sumMetric(metrics, `${prefix}_relay_event_deletions_total`);
296
297 document.getElementById('uptime').textContent =
298 formatUptime(Date.now() - startTime);
299
300 document.getElementById('error').innerHTML = '';
301 } catch (error) {
302 document.getElementById('error').innerHTML =
303 `<div class="error-msg">Failed to load metrics: ${error.message}</div>`;
304 }
305 }
306
307 updateMetrics();
308 setInterval(updateMetrics, 5000);
309 setInterval(() => {
310 document.getElementById('uptime').textContent = formatUptime(Date.now() - startTime);
311 }, 1000);
312 </script>
313</body>
314</html>