query("SELECT value FROM config WHERE key = 'relay_name'")->fetchColumn() ?: 'C-Relay-PG'; $relay_version = $pdo->query("SELECT value FROM config WHERE key = 'relay_version'")->fetchColumn() ?: ''; } catch (PDOException $e) {} $display_name = $relay_version ? "$relay_name v$relay_version" : $relay_name; // --- Total events --- $total_events = 0; try { $total_events = intval($pdo->query("SELECT COUNT(*) FROM events")->fetchColumn()); } catch (PDOException $e) {} // --- Database size --- $db_size = '-'; try { $bytes = intval($pdo->query("SELECT pg_database_size(current_database())")->fetchColumn()); if ($bytes < 1073741824) { $db_size = round($bytes / 1048576, 1) . ' MB'; } else { $db_size = round($bytes / 1073741824, 2) . ' GB'; } } catch (PDOException $e) {} // --- Process ID --- $process_id = '-'; try { $pid = $pdo->query("SELECT pg_backend_pid()")->fetchColumn(); $process_id = strval($pid); } catch (PDOException $e) {} // --- WebSocket connections (estimate from pg_class) --- $ws_connections = 0; try { $est = intval($pdo->query("SELECT reltuples FROM pg_class WHERE relname = 'subscriptions'")->fetchColumn()); $ws_connections = max(0, intval($est * 0.2)); } catch (PDOException $e) {} // --- Active subscriptions (same estimate) --- $active_subscriptions = $ws_connections; // --- Memory usage --- $memory_usage = '-'; $mem_total = 0; $mem_avail = 0; $meminfo = @file_get_contents('/proc/meminfo'); if ($meminfo !== false) { // /proc/meminfo uses "Key: Value kB" format (colon, not equals sign) foreach (explode("\n", $meminfo) as $line) { if (preg_match('/^MemTotal:\s+(\d+)\s+kB/i', $line, $m)) { $mem_total = intval($m[1]) * 1024; } elseif (preg_match('/^MemAvailable:\s+(\d+)\s+kB/i', $line, $m)) { $mem_avail = intval($m[1]) * 1024; } } } if ($mem_total > 0) { $used = $mem_total - $mem_avail; if ($used < 1073741824) { $memory_usage = round($used / 1048576, 1) . ' MB / ' . round($mem_total / 1073741824, 2) . ' GB'; } else { $memory_usage = round($used / 1073741824, 2) . ' GB / ' . round($mem_total / 1073741824, 2) . ' GB'; } } // --- CPU --- $cpu_usage = '-'; $cpu_core = '-'; if (is_readable('/proc/cpuinfo')) { $cores = intval(@shell_exec('nproc 2>/dev/null') ?: 1); $cpu_core = strval($cores) . ' cores'; } $load = @file_get_contents('/proc/loadavg'); if ($load !== false) { $cpu_usage = trim(explode(' ', $load)[0] ?? '-'); } // --- Oldest / newest event --- $oldest_event = '-'; $newest_event = '-'; try { $row = $pdo->query("SELECT MIN(created_at) AS oldest, MAX(created_at) AS newest FROM events")->fetch(); if ($row && $row['oldest']) $oldest_event = date('Y-m-d H:i:s', intval($row['oldest'])); if ($row && $row['newest']) $newest_event = date('Y-m-d H:i:s', intval($row['newest'])); } catch (PDOException $e) {} // --- Time-based stats --- $now = time(); $events_24h = 0; $events_7d = 0; $events_30d = 0; try { $events_24h = intval($pdo->query("SELECT COUNT(*) FROM events WHERE created_at >= $now - 86400")->fetchColumn()); $events_7d = intval($pdo->query("SELECT COUNT(*) FROM events WHERE created_at >= $now - 604800")->fetchColumn()); $events_30d = intval($pdo->query("SELECT COUNT(*) FROM events WHERE created_at >= $now - 2592000")->fetchColumn()); } catch (PDOException $e) {} // --- Kind distribution (top 10) --- $kinds = []; try { $rows = $pdo->query("SELECT kind, COUNT(*) AS cnt FROM events GROUP BY kind ORDER BY cnt DESC LIMIT 10")->fetchAll(); foreach ($rows as $r) { $kinds[] = ['kind' => intval($r['kind']), 'count' => intval($r['cnt']), 'pct' => $total_events > 0 ? round(intval($r['cnt']) / $total_events * 100, 1) : 0]; } } catch (PDOException $e) {} // --- Top pubkeys (top 20) --- $top_pubkeys = []; try { $rows = $pdo->query(" SELECT pubkey, COUNT(*) AS cnt FROM events GROUP BY pubkey ORDER BY cnt DESC LIMIT 20 ")->fetchAll(); $pubkeys = array_column($rows, 'pubkey'); $pmap = profile_map($pubkeys); foreach ($rows as $r) { $pk = $r['pubkey']; $prof = $pmap[$pk] ?? null; $top_pubkeys[] = [ 'pubkey' => $pk, 'name' => $prof ? $prof['best_name'] : '', 'count' => intval($r['cnt']), 'pct' => $total_events > 0 ? round(intval($r['cnt']) / $total_events * 100, 1) : 0, ]; } } catch (PDOException $e) {} // --- 1H ASCII chart (30 bins of 2 minutes each) --- $chart = ''; try { $span = 3600; $bin_size = 120; // 2 minutes $num_bins = 30; $epoch = $now - $span; $base_bin = (int)floor($epoch / $bin_size); $sql = "SELECT FLOOR(created_at / {$bin_size})::BIGINT - {$base_bin} AS bin, COUNT(*) AS cnt FROM events WHERE created_at >= {$epoch} GROUP BY bin ORDER BY bin"; $rows = $pdo->query($sql)->fetchAll(); $bins = build_bin_array($rows, $num_bins); $bins = array_reverse($bins); $chart = render_ascii_chart($bins, [ 'title' => 'Event Rate (Last Hour)', 'max_height' => 11, 'bin_duration' => $bin_size, 'label_interval' => 5, // label every 5 bins (10 min) ]); } catch (PDOException $e) { $chart = "Chart unavailable\n"; } // ================================ // BUILD MARKDOWN OUTPUT // ================================ $output = ''; // Chart $output .= "## laantungir.net/relay\n\n"; $output .= "```\n$chart```\n"; // Database Overview $output .= "## Database Overview\n\n"; $output .= "| Metric | Value |\n"; $output .= "|--------|-------|\n"; $output .= "| Database Size | $db_size |\n"; $output .= "| Total Events | " . number_format($total_events) . " |\n"; $output .= "| Process ID | $process_id |\n"; $output .= "| WebSocket Connections | " . number_format($ws_connections) . " |\n"; $output .= "| Active Subscriptions | " . number_format($active_subscriptions) . " |\n"; $output .= "| Memory Usage | $memory_usage |\n"; $output .= "| CPU Usage | $cpu_usage |\n"; $output .= "| CPU Core | $cpu_core |\n"; $output .= "| Oldest Event | $oldest_event |\n"; $output .= "| Newest Event | $newest_event |\n\n"; // Event Kinds (Top 10) $output .= "## Event Kinds (Top 10)\n\n"; $output .= "| Kind | Count | % |\n"; $output .= "|------|-------|---|\n"; foreach ($kinds as $k) { $output .= "| {$k['kind']} | " . number_format($k['count']) . " | {$k['pct']}% |\n"; } $output .= "\n"; // Time-Based Statistics $output .= "## Time-Based Statistics\n\n"; $output .= "| Period | Events |\n"; $output .= "|--------|-------|\n"; $output .= "| 24 Hours | " . number_format($events_24h) . " |\n"; $output .= "| 7 Days | " . number_format($events_7d) . " |\n"; $output .= "| 30 Days | " . number_format($events_30d) . " |\n\n"; // Top Pubkeys $output .= "## Top Pubkeys\n\n"; $output .= "| Name | Events | % |\n"; $output .= "|------|-------|---|\n"; foreach ($top_pubkeys as $p) { if ($p['name']) { $name = $p['name']; } else { $name = substr($p['pubkey'], 0, 4) . '..' . substr($p['pubkey'], -4); } $output .= "| $name | " . number_format($p['count']) . " | {$p['pct']}% |\n"; } $output .= "\n"; // Footer $output .= "---\n"; $output .= "_Report generated: " . date('Y-m-d H:i:s T') . "_\n"; echo $output;