506 lines
24 KiB
C
506 lines
24 KiB
C
|
|
#include "dashboard.h"
|
||
|
|
#include "buffer.h"
|
||
|
|
#include "monitor.h"
|
||
|
|
#include "connection.h"
|
||
|
|
#include "../cJSON.h"
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <sys/epoll.h>
|
||
|
|
|
||
|
|
static const char *DASHBOARD_HTML =
|
||
|
|
"<!DOCTYPE html>\n"
|
||
|
|
"<html>\n"
|
||
|
|
"<head>\n"
|
||
|
|
" <title>Reverse Proxy Monitor</title>\n"
|
||
|
|
" <style>\n"
|
||
|
|
" * { margin: 0; padding: 0; box-sizing: border-box; }\n"
|
||
|
|
" body { font-family: -apple-system, system-ui, sans-serif; background: #000; color: #fff; padding: 20px; }\n"
|
||
|
|
" .header { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); margin-bottom: 30px; gap: 20px; }\n"
|
||
|
|
" .metric { text-align: center; background: #000; border-radius: 8px; padding: 15px; }\n"
|
||
|
|
" .metric-value { font-size: 36px; font-weight: 300; }\n"
|
||
|
|
" .metric-label { font-size: 14px; opacity: 0.7; text-transform: uppercase; margin-top: 5px; }\n"
|
||
|
|
" .chart-container { background: #000; border-radius: 8px; padding: 20px; margin-bottom: 20px; height: 250px; position: relative; }\n"
|
||
|
|
" .chart-title { position: absolute; top: 10px; left: 20px; font-size: 14px; opacity: 0.7; z-index: 10; }\n"
|
||
|
|
" canvas { width: 100% !important; height: 100% !important; }\n"
|
||
|
|
" .process-table { background: #000; border-radius: 8px; padding: 20px; }\n"
|
||
|
|
" table { width: 100%; border-collapse: collapse; }\n"
|
||
|
|
" th, td { padding: 10px; text-align: left; border-bottom: 1px solid #2a2e3e; }\n"
|
||
|
|
" th { font-weight: 500; opacity: 0.7; }\n"
|
||
|
|
" .legend { position: absolute; top: 10px; right: 20px; display: flex; gap: 20px; font-size: 12px; z-index: 10; }\n"
|
||
|
|
" .legend-item { display: flex; align-items: center; gap: 5px; }\n"
|
||
|
|
" .legend-color { width: 12px; height: 12px; border-radius: 2px; }\n"
|
||
|
|
" .vhost-chart-container { height: 200px; background: #000; border-radius: 8px; position: relative; padding: 20px; margin-bottom: 10px; }\n"
|
||
|
|
" .load-values { display: flex; gap: 10px; font-size: 12px; opacity: 0.8; }\n"
|
||
|
|
" </style>\n"
|
||
|
|
" <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n"
|
||
|
|
"</head>\n"
|
||
|
|
"<body>\n"
|
||
|
|
" <div class=\"header\">\n"
|
||
|
|
" <div class=\"metric\">\n"
|
||
|
|
" <div class=\"metric-value\" id=\"connections\">0</div>\n"
|
||
|
|
" <div class=\"metric-label\">Connections</div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
" <div class=\"metric\">\n"
|
||
|
|
" <div class=\"metric-value\" id=\"memory\">0</div>\n"
|
||
|
|
" <div class=\"metric-label\">Memory</div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
" <div class=\"metric\">\n"
|
||
|
|
" <div class=\"metric-value\" id=\"cpu\">0</div>\n"
|
||
|
|
" <div class=\"metric-label\">CPU %</div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
" <div class=\"metric\">\n"
|
||
|
|
" <div class=\"metric-value\" id=\"load1\">0.00</div>\n"
|
||
|
|
" <div class=\"metric-label\">Load 1m</div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
" <div class=\"metric\">\n"
|
||
|
|
" <div class=\"metric-value\" id=\"load5\">0.00</div>\n"
|
||
|
|
" <div class=\"metric-label\">Load 5m</div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
" <div class=\"metric\">\n"
|
||
|
|
" <div class=\"metric-value\" id=\"load15\">0.00</div>\n"
|
||
|
|
" <div class=\"metric-label\">Load 15m</div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
"\n"
|
||
|
|
" <div class=\"chart-container\">\n"
|
||
|
|
" <div class=\"chart-title\">CPU Usage</div>\n"
|
||
|
|
" <canvas id=\"cpuChart\"></canvas>\n"
|
||
|
|
" </div>\n"
|
||
|
|
"\n"
|
||
|
|
" <div class=\"chart-container\">\n"
|
||
|
|
" <div class=\"chart-title\">Memory Usage</div>\n"
|
||
|
|
" <canvas id=\"memChart\"></canvas>\n"
|
||
|
|
" </div>\n"
|
||
|
|
"\n"
|
||
|
|
" <div class=\"chart-container\">\n"
|
||
|
|
" <div class=\"chart-title\">Network I/O</div>\n"
|
||
|
|
" <div class=\"legend\">\n"
|
||
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #3498db\"></div><span>RX</span></div>\n"
|
||
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #2ecc71\"></div><span>TX</span></div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
" <canvas id=\"netChart\"></canvas>\n"
|
||
|
|
" </div>\n"
|
||
|
|
"\n"
|
||
|
|
" <div class=\"chart-container\">\n"
|
||
|
|
" <div class=\"chart-title\">Disk I/O</div>\n"
|
||
|
|
" <div class=\"legend\">\n"
|
||
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #9b59b6\"></div><span>Read</span></div>\n"
|
||
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #e67e22\"></div><span>Write</span></div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
" <canvas id=\"diskChart\"></canvas>\n"
|
||
|
|
" </div>\n"
|
||
|
|
"\n"
|
||
|
|
" <div class=\"chart-container\">\n"
|
||
|
|
" <div class=\"chart-title\">Load Average</div>\n"
|
||
|
|
" <div class=\"legend\">\n"
|
||
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #e74c3c\"></div><span>1 min</span></div>\n"
|
||
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #f39c12\"></div><span>5 min</span></div>\n"
|
||
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #3498db\"></div><span>15 min</span></div>\n"
|
||
|
|
" </div>\n"
|
||
|
|
" <canvas id=\"loadChart\"></canvas>\n"
|
||
|
|
" </div>\n"
|
||
|
|
"\n"
|
||
|
|
" <div class=\"process-table\">\n"
|
||
|
|
" <table>\n"
|
||
|
|
" <thead>\n"
|
||
|
|
" <tr>\n"
|
||
|
|
" <th>Virtual Host</th>\n"
|
||
|
|
" <th>HTTP Req</th>\n"
|
||
|
|
" <th>WS Req</th>\n"
|
||
|
|
" <th>Total Req</th>\n"
|
||
|
|
" <th>Avg Resp (ms)</th>\n"
|
||
|
|
" <th>Sent</th>\n"
|
||
|
|
" <th>Received</th>\n"
|
||
|
|
" </tr>\n"
|
||
|
|
" </thead>\n"
|
||
|
|
" <tbody id=\"processTable\"></tbody>\n"
|
||
|
|
" </table>\n"
|
||
|
|
" </div>\n"
|
||
|
|
"\n"
|
||
|
|
" <script>\n"
|
||
|
|
" const formatTimeTick = (value) => {\n"
|
||
|
|
" const seconds = Math.abs(Math.round(value / 1000));\n"
|
||
|
|
" if (seconds === 0) return 'now';\n"
|
||
|
|
" const m = Math.floor(seconds / 60);\n"
|
||
|
|
" const s = seconds % 60;\n"
|
||
|
|
" return `-${m > 0 ? `${m}m ` : ''}${s}s`;\n"
|
||
|
|
" };\n"
|
||
|
|
"\n"
|
||
|
|
" const formatSizeTick = (value) => {\n"
|
||
|
|
" if (value >= 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} GB/s`;\n"
|
||
|
|
" if (value >= 1024) return `${(value / 1024).toFixed(1)} MB/s`;\n"
|
||
|
|
" return `${value.toFixed(0)} KB/s`;\n"
|
||
|
|
" };\n"
|
||
|
|
"\n"
|
||
|
|
" const formatDiskTick = (value) => {\n"
|
||
|
|
" if (value >= 1024) return `${(value / 1024).toFixed(1)} GB/s`;\n"
|
||
|
|
" return `${value.toFixed(1)} MB/s`;\n"
|
||
|
|
" };\n"
|
||
|
|
"\n"
|
||
|
|
" const formatBytes = (bytes) => {\n"
|
||
|
|
" if (bytes === 0) return '0 B';\n"
|
||
|
|
" const k = 1024;\n"
|
||
|
|
" const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n"
|
||
|
|
" const i = Math.floor(Math.log(bytes) / Math.log(k));\n"
|
||
|
|
" return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n"
|
||
|
|
" };\n"
|
||
|
|
"\n"
|
||
|
|
" const createBaseChartOptions = (historySeconds, yTickCallback) => ({\n"
|
||
|
|
" responsive: true,\n"
|
||
|
|
" maintainAspectRatio: false,\n"
|
||
|
|
" animation: false,\n"
|
||
|
|
" layout: { padding: { top: 30 } },\n"
|
||
|
|
" interaction: { mode: 'nearest', axis: 'x', intersect: false },\n"
|
||
|
|
" scales: {\n"
|
||
|
|
" x: { type: 'linear', display: true, grid: { color: '#2a2e3e' }, ticks: { color: '#666', maxTicksLimit: 7, callback: formatTimeTick }, min: -historySeconds * 1000, max: 0 },\n"
|
||
|
|
" y: { display: true, grid: { color: '#2a2e3e' }, ticks: { color: '#666', beginAtZero: true, callback: yTickCallback } }\n"
|
||
|
|
" },\n"
|
||
|
|
" plugins: { legend: { display: false }, tooltip: { displayColors: false } },\n"
|
||
|
|
" elements: { point: { radius: 0 }, line: { borderWidth: 2, tension: 0.4, fill: true } }\n"
|
||
|
|
" });\n"
|
||
|
|
"\n"
|
||
|
|
" const cpuChart = new Chart(document.getElementById('cpuChart'), {\n"
|
||
|
|
" type: 'line',\n"
|
||
|
|
" data: { datasets: [{ label: 'CPU %', data: [], borderColor: '#f39c12', backgroundColor: 'rgba(243, 156, 18, 0.1)' }] },\n"
|
||
|
|
" options: { ...createBaseChartOptions(300, v => `${v}%`), scales: { ...createBaseChartOptions(300).scales, y: { ...createBaseChartOptions(300).scales.y, max: 100, ticks: { ...createBaseChartOptions(300).scales.y.ticks, callback: v => `${v}%` } } } }\n"
|
||
|
|
" });\n"
|
||
|
|
"\n"
|
||
|
|
" const memChart = new Chart(document.getElementById('memChart'), {\n"
|
||
|
|
" type: 'line',\n"
|
||
|
|
" data: { datasets: [{ label: 'Memory GB', data: [], borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)' }] },\n"
|
||
|
|
" options: createBaseChartOptions(300, v => `${v.toFixed(2)} GiB`)\n"
|
||
|
|
" });\n"
|
||
|
|
"\n"
|
||
|
|
" const netChart = new Chart(document.getElementById('netChart'), {\n"
|
||
|
|
" type: 'line',\n"
|
||
|
|
" data: {\n"
|
||
|
|
" datasets: [\n"
|
||
|
|
" { label: 'RX KB/s', data: [], borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)' },\n"
|
||
|
|
" { label: 'TX KB/s', data: [], borderColor: '#2ecc71', backgroundColor: 'rgba(46, 204, 113, 0.1)' }\n"
|
||
|
|
" ]\n"
|
||
|
|
" },\n"
|
||
|
|
" options: createBaseChartOptions(300, formatSizeTick)\n"
|
||
|
|
" });\n"
|
||
|
|
"\n"
|
||
|
|
" const diskChart = new Chart(document.getElementById('diskChart'), {\n"
|
||
|
|
" type: 'line',\n"
|
||
|
|
" data: {\n"
|
||
|
|
" datasets: [\n"
|
||
|
|
" { label: 'Read MB/s', data: [], borderColor: '#9b59b6', backgroundColor: 'rgba(155, 89, 182, 0.1)' },\n"
|
||
|
|
" { label: 'Write MB/s', data: [], borderColor: '#e67e22', backgroundColor: 'rgba(230, 126, 34, 0.1)' }\n"
|
||
|
|
" ]\n"
|
||
|
|
" },\n"
|
||
|
|
" options: createBaseChartOptions(300, formatDiskTick)\n"
|
||
|
|
" });\n"
|
||
|
|
"\n"
|
||
|
|
" const loadChart = new Chart(document.getElementById('loadChart'), {\n"
|
||
|
|
" type: 'line',\n"
|
||
|
|
" data: {\n"
|
||
|
|
" datasets: [\n"
|
||
|
|
" { label: 'Load 1m', data: [], borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)' },\n"
|
||
|
|
" { label: 'Load 5m', data: [], borderColor: '#f39c12', backgroundColor: 'rgba(243, 156, 18, 0.1)' },\n"
|
||
|
|
" { label: 'Load 15m', data: [], borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)' }\n"
|
||
|
|
" ]\n"
|
||
|
|
" },\n"
|
||
|
|
" options: createBaseChartOptions(300, v => v.toFixed(2))\n"
|
||
|
|
" });\n"
|
||
|
|
"\n"
|
||
|
|
" window.vhostCharts = {};\n"
|
||
|
|
" let prevProcessNames = [];\n"
|
||
|
|
"\n"
|
||
|
|
" async function updateStats() {\n"
|
||
|
|
" try {\n"
|
||
|
|
" const response = await fetch('/rproxy/api/stats');\n"
|
||
|
|
" const data = await response.json();\n"
|
||
|
|
"\n"
|
||
|
|
" document.getElementById('connections').textContent = data.current.active_connections;\n"
|
||
|
|
" document.getElementById('memory').textContent = data.current.memory_gb + ' GiB';\n"
|
||
|
|
" document.getElementById('cpu').textContent = data.current.cpu_percent + '%';\n"
|
||
|
|
" document.getElementById('load1').textContent = data.current.load_1m.toFixed(2);\n"
|
||
|
|
" document.getElementById('load5').textContent = data.current.load_5m.toFixed(2);\n"
|
||
|
|
" document.getElementById('load15').textContent = data.current.load_15m.toFixed(2);\n"
|
||
|
|
"\n"
|
||
|
|
" cpuChart.data.datasets[0].data = data.cpu_history;\n"
|
||
|
|
" memChart.data.datasets[0].data = data.memory_history;\n"
|
||
|
|
" netChart.data.datasets[0].data = data.network_rx_history;\n"
|
||
|
|
" netChart.data.datasets[1].data = data.network_tx_history;\n"
|
||
|
|
" diskChart.data.datasets[0].data = data.disk_read_history;\n"
|
||
|
|
" diskChart.data.datasets[1].data = data.disk_write_history;\n"
|
||
|
|
" loadChart.data.datasets[0].data = data.load1_history;\n"
|
||
|
|
" loadChart.data.datasets[1].data = data.load5_history;\n"
|
||
|
|
" loadChart.data.datasets[2].data = data.load15_history;\n"
|
||
|
|
"\n"
|
||
|
|
" cpuChart.update('none');\n"
|
||
|
|
" memChart.update('none');\n"
|
||
|
|
" netChart.update('none');\n"
|
||
|
|
" diskChart.update('none');\n"
|
||
|
|
" loadChart.update('none');\n"
|
||
|
|
"\n"
|
||
|
|
" const tbody = document.getElementById('processTable');\n"
|
||
|
|
" const processNames = data.processes.map(p => p.name).join(',');\n"
|
||
|
|
" if (processNames !== prevProcessNames.join(',')) {\n"
|
||
|
|
" Object.values(window.vhostCharts).forEach(chart => chart.destroy());\n"
|
||
|
|
" window.vhostCharts = {};\n"
|
||
|
|
" tbody.innerHTML = '';\n"
|
||
|
|
" data.processes.forEach((p, index) => {\n"
|
||
|
|
" const colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#9b59b6', '#1abc9c'];\n"
|
||
|
|
" const chartColor = colors[index % colors.length];\n"
|
||
|
|
" const mainRow = document.createElement('tr');\n"
|
||
|
|
" mainRow.innerHTML = `<td style='color: ${chartColor}'>${p.name}</td><td>${p.http_requests}</td><td>${p.websocket_requests}</td><td>${p.total_requests}</td><td>${p.avg_request_time_ms.toFixed(2)}</td><td>${formatBytes(p.bytes_sent)}</td><td>${formatBytes(p.bytes_recv)}</td>`;\n"
|
||
|
|
" tbody.appendChild(mainRow);\n"
|
||
|
|
" \n"
|
||
|
|
" const chartRow = document.createElement('tr');\n"
|
||
|
|
" chartRow.innerHTML = `<td colspan='7' style='padding: 10px 0; border: none;'><div class='vhost-chart-container'><div class='chart-title'>Live Throughput - ${p.name}</div><canvas id='vhostChart${index}'></canvas></div></td>`;\n"
|
||
|
|
" tbody.appendChild(chartRow);\n"
|
||
|
|
" });\n"
|
||
|
|
" prevProcessNames = data.processes.map(p => p.name);\n"
|
||
|
|
" }\n"
|
||
|
|
"\n"
|
||
|
|
" data.processes.forEach((p, index) => {\n"
|
||
|
|
" const chartId = `vhostChart${index}`;\n"
|
||
|
|
" const canvas = document.getElementById(chartId);\n"
|
||
|
|
" if (!canvas) return;\n"
|
||
|
|
" if (!window.vhostCharts[chartId]) {\n"
|
||
|
|
" const colors = [\n"
|
||
|
|
" { border: '#3498db', bg: 'rgba(52, 152, 219, 0.1)' }, { border: '#2ecc71', bg: 'rgba(46, 204, 113, 0.1)' },\n"
|
||
|
|
" { border: '#f39c12', bg: 'rgba(243, 156, 18, 0.1)' }, { border: '#e74c3c', bg: 'rgba(231, 76, 60, 0.1)' },\n"
|
||
|
|
" { border: '#9b59b6', bg: 'rgba(155, 89, 182, 0.1)' }, { border: '#1abc9c', bg: 'rgba(26, 188, 156, 0.1)' }\n"
|
||
|
|
" ];\n"
|
||
|
|
" const color = colors[index % colors.length];\n"
|
||
|
|
" window.vhostCharts[chartId] = new Chart(canvas.getContext('2d'), {\n"
|
||
|
|
" type: 'line',\n"
|
||
|
|
" data: { datasets: [{ label: 'Throughput KB/s', data: p.throughput_history || [], borderColor: color.border, backgroundColor: color.bg }] },\n"
|
||
|
|
" options: createBaseChartOptions(60, formatSizeTick)\n"
|
||
|
|
" });\n"
|
||
|
|
" } else {\n"
|
||
|
|
" window.vhostCharts[chartId].data.datasets[0].data = p.throughput_history || [];\n"
|
||
|
|
" window.vhostCharts[chartId].update('none');\n"
|
||
|
|
" }\n"
|
||
|
|
" });\n"
|
||
|
|
" } catch (e) {\n"
|
||
|
|
" console.error('Failed to fetch stats:', e);\n"
|
||
|
|
" }\n"
|
||
|
|
" }\n"
|
||
|
|
"\n"
|
||
|
|
" updateStats();\n"
|
||
|
|
" setInterval(updateStats, 1000);\n"
|
||
|
|
" </script>\n"
|
||
|
|
"</body>\n"
|
||
|
|
"</html>\n";
|
||
|
|
|
||
|
|
void dashboard_serve(connection_t *conn) {
|
||
|
|
if (!conn) return;
|
||
|
|
|
||
|
|
size_t content_len = strlen(DASHBOARD_HTML);
|
||
|
|
char header[512];
|
||
|
|
int len = snprintf(header, sizeof(header),
|
||
|
|
"HTTP/1.1 200 OK\r\n"
|
||
|
|
"Content-Type: text/html; charset=utf-8\r\n"
|
||
|
|
"Content-Length: %zu\r\n"
|
||
|
|
"Connection: %s\r\n"
|
||
|
|
"Cache-Control: no-cache\r\n"
|
||
|
|
"\r\n",
|
||
|
|
content_len,
|
||
|
|
conn->request.keep_alive ? "keep-alive" : "close");
|
||
|
|
|
||
|
|
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + len + content_len) < 0) {
|
||
|
|
connection_send_error_response(conn, 500, "Internal Server Error", "Memory allocation failed");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
memcpy(conn->write_buf.data + conn->write_buf.tail, header, len);
|
||
|
|
conn->write_buf.tail += len;
|
||
|
|
memcpy(conn->write_buf.data + conn->write_buf.tail, DASHBOARD_HTML, content_len);
|
||
|
|
conn->write_buf.tail += content_len;
|
||
|
|
|
||
|
|
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
|
||
|
|
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
|
||
|
|
}
|
||
|
|
|
||
|
|
static cJSON* format_history(history_deque_t *dq, int window_seconds) {
|
||
|
|
cJSON *arr = cJSON_CreateArray();
|
||
|
|
if (!arr || !dq || !dq->points || dq->count == 0) return arr;
|
||
|
|
|
||
|
|
double current_time = time(NULL);
|
||
|
|
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
|
||
|
|
|
||
|
|
for (int i = 0; i < dq->count; ++i) {
|
||
|
|
int current_index = (start_index + i) % dq->capacity;
|
||
|
|
history_point_t *p = &dq->points[current_index];
|
||
|
|
if ((current_time - p->time) <= window_seconds) {
|
||
|
|
cJSON *pt = cJSON_CreateObject();
|
||
|
|
if (pt) {
|
||
|
|
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
|
||
|
|
cJSON_AddNumberToObject(pt, "y", p->value);
|
||
|
|
cJSON_AddItemToArray(arr, pt);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return arr;
|
||
|
|
}
|
||
|
|
|
||
|
|
static cJSON* format_network_history(network_history_deque_t *dq, int window_seconds, const char *key) {
|
||
|
|
cJSON *arr = cJSON_CreateArray();
|
||
|
|
if (!arr || !dq || !dq->points || !key || dq->count == 0) return arr;
|
||
|
|
|
||
|
|
double current_time = time(NULL);
|
||
|
|
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
|
||
|
|
|
||
|
|
for (int i = 0; i < dq->count; ++i) {
|
||
|
|
int current_index = (start_index + i) % dq->capacity;
|
||
|
|
network_history_point_t *p = &dq->points[current_index];
|
||
|
|
if ((current_time - p->time) <= window_seconds) {
|
||
|
|
cJSON *pt = cJSON_CreateObject();
|
||
|
|
if (pt) {
|
||
|
|
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
|
||
|
|
cJSON_AddNumberToObject(pt, "y", strcmp(key, "rx_kbps") == 0 ? p->rx_kbps : p->tx_kbps);
|
||
|
|
cJSON_AddItemToArray(arr, pt);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return arr;
|
||
|
|
}
|
||
|
|
|
||
|
|
static cJSON* format_disk_history(disk_history_deque_t *dq, int window_seconds, const char *key) {
|
||
|
|
cJSON *arr = cJSON_CreateArray();
|
||
|
|
if (!arr || !dq || !dq->points || !key || dq->count == 0) return arr;
|
||
|
|
|
||
|
|
double current_time = time(NULL);
|
||
|
|
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
|
||
|
|
|
||
|
|
for (int i = 0; i < dq->count; ++i) {
|
||
|
|
int current_index = (start_index + i) % dq->capacity;
|
||
|
|
disk_history_point_t *p = &dq->points[current_index];
|
||
|
|
if ((current_time - p->time) <= window_seconds) {
|
||
|
|
cJSON *pt = cJSON_CreateObject();
|
||
|
|
if (pt) {
|
||
|
|
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
|
||
|
|
cJSON_AddNumberToObject(pt, "y", strcmp(key, "read_mbps") == 0 ? p->read_mbps : p->write_mbps);
|
||
|
|
cJSON_AddItemToArray(arr, pt);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return arr;
|
||
|
|
}
|
||
|
|
|
||
|
|
void dashboard_serve_stats_api(connection_t *conn) {
|
||
|
|
if (!conn) return;
|
||
|
|
|
||
|
|
cJSON *root = cJSON_CreateObject();
|
||
|
|
if (!root) {
|
||
|
|
connection_send_error_response(conn, 500, "Internal Server Error", "JSON creation failed");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
cJSON *current = cJSON_CreateObject();
|
||
|
|
if (!current) {
|
||
|
|
cJSON_Delete(root);
|
||
|
|
connection_send_error_response(conn, 500, "Internal Server Error", "JSON creation failed");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
cJSON_AddItemToObject(root, "current", current);
|
||
|
|
|
||
|
|
char buffer[64];
|
||
|
|
double last_cpu = 0, last_mem = 0;
|
||
|
|
double load1 = 0, load5 = 0, load15 = 0;
|
||
|
|
|
||
|
|
if (monitor.cpu_history.count > 0) {
|
||
|
|
int last_idx = (monitor.cpu_history.head - 1 + monitor.cpu_history.capacity) % monitor.cpu_history.capacity;
|
||
|
|
last_cpu = monitor.cpu_history.points[last_idx].value;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (monitor.memory_history.count > 0) {
|
||
|
|
int last_idx = (monitor.memory_history.head - 1 + monitor.memory_history.capacity) % monitor.memory_history.capacity;
|
||
|
|
last_mem = monitor.memory_history.points[last_idx].value;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (monitor.load1_history.count > 0) {
|
||
|
|
int idx = (monitor.load1_history.head - 1 + monitor.load1_history.capacity) % monitor.load1_history.capacity;
|
||
|
|
load1 = monitor.load1_history.points[idx].value;
|
||
|
|
}
|
||
|
|
if (monitor.load5_history.count > 0) {
|
||
|
|
int idx = (monitor.load5_history.head - 1 + monitor.load5_history.capacity) % monitor.load5_history.capacity;
|
||
|
|
load5 = monitor.load5_history.points[idx].value;
|
||
|
|
}
|
||
|
|
if (monitor.load15_history.count > 0) {
|
||
|
|
int idx = (monitor.load15_history.head - 1 + monitor.load15_history.capacity) % monitor.load15_history.capacity;
|
||
|
|
load15 = monitor.load15_history.points[idx].value;
|
||
|
|
}
|
||
|
|
|
||
|
|
snprintf(buffer, sizeof(buffer), "%.2f", last_cpu);
|
||
|
|
cJSON_AddStringToObject(current, "cpu_percent", buffer);
|
||
|
|
snprintf(buffer, sizeof(buffer), "%.2f", last_mem);
|
||
|
|
cJSON_AddStringToObject(current, "memory_gb", buffer);
|
||
|
|
cJSON_AddNumberToObject(current, "active_connections", monitor.active_connections);
|
||
|
|
cJSON_AddNumberToObject(current, "load_1m", load1);
|
||
|
|
cJSON_AddNumberToObject(current, "load_5m", load5);
|
||
|
|
cJSON_AddNumberToObject(current, "load_15m", load15);
|
||
|
|
|
||
|
|
cJSON_AddItemToObject(root, "cpu_history", format_history(&monitor.cpu_history, HISTORY_SECONDS));
|
||
|
|
cJSON_AddItemToObject(root, "memory_history", format_history(&monitor.memory_history, HISTORY_SECONDS));
|
||
|
|
cJSON_AddItemToObject(root, "network_rx_history", format_network_history(&monitor.network_history, HISTORY_SECONDS, "rx_kbps"));
|
||
|
|
cJSON_AddItemToObject(root, "network_tx_history", format_network_history(&monitor.network_history, HISTORY_SECONDS, "tx_kbps"));
|
||
|
|
cJSON_AddItemToObject(root, "disk_read_history", format_disk_history(&monitor.disk_history, HISTORY_SECONDS, "read_mbps"));
|
||
|
|
cJSON_AddItemToObject(root, "disk_write_history", format_disk_history(&monitor.disk_history, HISTORY_SECONDS, "write_mbps"));
|
||
|
|
cJSON_AddItemToObject(root, "throughput_history", format_history(&monitor.throughput_history, HISTORY_SECONDS));
|
||
|
|
cJSON_AddItemToObject(root, "load1_history", format_history(&monitor.load1_history, HISTORY_SECONDS));
|
||
|
|
cJSON_AddItemToObject(root, "load5_history", format_history(&monitor.load5_history, HISTORY_SECONDS));
|
||
|
|
cJSON_AddItemToObject(root, "load15_history", format_history(&monitor.load15_history, HISTORY_SECONDS));
|
||
|
|
|
||
|
|
cJSON *processes = cJSON_CreateArray();
|
||
|
|
if (processes) {
|
||
|
|
cJSON_AddItemToObject(root, "processes", processes);
|
||
|
|
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
|
||
|
|
cJSON *p = cJSON_CreateObject();
|
||
|
|
if (p) {
|
||
|
|
cJSON_AddStringToObject(p, "name", s->vhost_name);
|
||
|
|
cJSON_AddNumberToObject(p, "http_requests", s->http_requests);
|
||
|
|
cJSON_AddNumberToObject(p, "websocket_requests", s->websocket_requests);
|
||
|
|
cJSON_AddNumberToObject(p, "total_requests", s->total_requests);
|
||
|
|
cJSON_AddNumberToObject(p, "avg_request_time_ms", s->avg_request_time_ms);
|
||
|
|
cJSON_AddNumberToObject(p, "bytes_sent", s->bytes_sent);
|
||
|
|
cJSON_AddNumberToObject(p, "bytes_recv", s->bytes_recv);
|
||
|
|
cJSON_AddItemToObject(p, "throughput_history", format_history(&s->throughput_history, 60));
|
||
|
|
cJSON_AddItemToArray(processes, p);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
char *json_string = cJSON_PrintUnformatted(root);
|
||
|
|
if (!json_string) {
|
||
|
|
cJSON_Delete(root);
|
||
|
|
connection_send_error_response(conn, 500, "Internal Server Error", "JSON serialization failed");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
char header[512];
|
||
|
|
int hlen = snprintf(header, sizeof(header),
|
||
|
|
"HTTP/1.1 200 OK\r\n"
|
||
|
|
"Content-Type: application/json; charset=utf-8\r\n"
|
||
|
|
"Content-Length: %zu\r\n"
|
||
|
|
"Connection: %s\r\n"
|
||
|
|
"Cache-Control: no-cache\r\n"
|
||
|
|
"\r\n",
|
||
|
|
strlen(json_string),
|
||
|
|
conn->request.keep_alive ? "keep-alive" : "close");
|
||
|
|
|
||
|
|
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + hlen + strlen(json_string)) < 0) {
|
||
|
|
free(json_string);
|
||
|
|
cJSON_Delete(root);
|
||
|
|
connection_send_error_response(conn, 500, "Internal Server Error", "Memory allocation failed");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
memcpy(conn->write_buf.data + conn->write_buf.tail, header, hlen);
|
||
|
|
conn->write_buf.tail += hlen;
|
||
|
|
memcpy(conn->write_buf.data + conn->write_buf.tail, json_string, strlen(json_string));
|
||
|
|
conn->write_buf.tail += strlen(json_string);
|
||
|
|
|
||
|
|
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
|
||
|
|
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
|
||
|
|
|
||
|
|
cJSON_Delete(root);
|
||
|
|
free(json_string);
|
||
|
|
}
|