diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/metrics/dashboard.html | 314 | ||||
| -rw-r--r-- | internal/metrics/metrics.go | 8 |
2 files changed, 322 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> | ||
diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 9030d67..9030775 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | package metrics | 1 | package metrics |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | _ "embed" | ||
| 4 | "net/http" | 5 | "net/http" |
| 5 | 6 | ||
| 6 | "github.com/prometheus/client_golang/prometheus" | 7 | "github.com/prometheus/client_golang/prometheus" |
| @@ -284,8 +285,15 @@ const ( | |||
| 284 | StatusInvalidRequest RequestStatus = "invalid_request" | 285 | StatusInvalidRequest RequestStatus = "invalid_request" |
| 285 | ) | 286 | ) |
| 286 | 287 | ||
| 288 | //go:embed dashboard.html | ||
| 289 | var dashboardHTML []byte | ||
| 290 | |||
| 287 | func (m *Metrics) Serve(addr, path string) error { | 291 | func (m *Metrics) Serve(addr, path string) error { |
| 288 | mux := http.NewServeMux() | 292 | mux := http.NewServeMux() |
| 289 | mux.Handle(path, promhttp.Handler()) | 293 | mux.Handle(path, promhttp.Handler()) |
| 294 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||
| 295 | w.Header().Set("Content-Type", "text/html; charset=utf-8") | ||
| 296 | w.Write(dashboardHTML) | ||
| 297 | }) | ||
| 290 | return http.ListenAndServe(addr, mux) | 298 | return http.ListenAndServe(addr, mux) |
| 291 | } | 299 | } |
