feat: integrate monitoring metrics into connection handling and redesign dashboard with advanced charts

Add real-time tracking of accepted connections, upstream connection success/failure, DNS errors, and retries to connection.c. Introduce histogram, rate tracker, and health score data structures in types.h and monitor.h/c. Redesign dashboard HTML/CSS with color-coded health metrics, responsive charts grid, and tall chart containers for improved visualization of system performance data.
This commit is contained in:
2025-12-29 00:37:24 +00:00
parent ca8bdcf2cd
commit 598668ca8a
10 changed files with 993 additions and 260 deletions
+46 -2
View File
@@ -215,6 +215,7 @@ void connection_accept(int listener_fd) {
}
monitor.active_connections++;
monitor.total_connections_accepted++;
log_debug("New connection on fd %d from %s, total: %d",
client_fd, conn->client_ip, monitor.active_connections);
}
@@ -527,6 +528,9 @@ void connection_connect_to_upstream(connection_t *client, const char *data, size
if (gai_err == 0 && result) freeaddrinfo(result);
log_debug("DNS resolution failed for %s: %s", route->upstream_host,
gai_err ? gai_strerror(gai_err) : "no address returned");
if (client->vhost_stats) {
monitor_record_error(client->vhost_stats, ERROR_TYPE_DNS);
}
connection_send_error_response(client, 502, "Bad Gateway", "Cannot resolve upstream hostname");
return;
}
@@ -555,10 +559,20 @@ void connection_connect_to_upstream(connection_t *client, const char *data, size
if (up_fd < 0) {
log_debug("All %d connection attempts failed for %s:%d",
MAX_UPSTREAM_RETRIES, route->upstream_host, route->upstream_port);
if (client->vhost_stats) {
monitor_record_upstream_connect(client->vhost_stats, 0, 0);
}
connection_send_error_response(client, 502, "Bad Gateway", "Failed to connect to upstream");
return;
}
if (client->vhost_stats) {
monitor_record_upstream_connect(client->vhost_stats, 1, 0);
if (retry_count > 0) {
client->vhost_stats->upstream_retries += retry_count;
}
}
connection_add_to_epoll(up_fd, EPOLLIN | EPOLLOUT);
connection_t *up = &connections[up_fd];
@@ -821,6 +835,10 @@ static void handle_client_read(connection_t *conn) {
conn->vhost_stats = monitor_get_or_create_vhost_stats(conn->request.host);
monitor_record_request_start(conn->vhost_stats, conn->request.is_websocket);
monitor_record_method(conn->vhost_stats, http_method_from_string(conn->request.method));
if (conn->request.content_length > 0) {
monitor_record_request_size(conn->vhost_stats, conn->request.content_length);
}
conn->state = CLIENT_STATE_FORWARDING;
connection_connect_to_upstream(conn, data_start, len_to_forward);
@@ -867,6 +885,7 @@ static ssize_t splice_forward(connection_t *conn, connection_t *pair) {
if (conn->vhost_stats) {
monitor_record_bytes(conn->vhost_stats, bytes_from_pipe, bytes_to_pipe);
monitor_record_splice_transfer(conn->vhost_stats, bytes_from_pipe);
}
return bytes_from_pipe;
@@ -998,10 +1017,19 @@ static void handle_forwarding(connection_t *conn) {
conn->original_content_length = http_get_content_length(src_data, headers_end);
conn->response_headers_parsed = 1;
log_debug("Response Content-Type: %s, textual: %d, content-length: %ld",
int status_code = http_extract_status_code(src_data, headers_end);
if (status_code > 0 && conn->vhost_stats) {
monitor_record_status(conn->vhost_stats, status_code);
}
if (conn->original_content_length > 0 && conn->vhost_stats) {
monitor_record_response_size(conn->vhost_stats, conn->original_content_length);
}
log_debug("Response Content-Type: %s, textual: %d, content-length: %ld, status: %d",
content_type[0] ? content_type : "(none)",
conn->is_textual_content,
conn->original_content_length);
conn->original_content_length,
status_code);
}
}
@@ -1084,6 +1112,10 @@ static void handle_forwarding(connection_t *conn) {
pair->write_buf.tail += output_len;
buffer_consume(&conn->read_buf, data_to_forward);
if (conn->vhost_stats) {
monitor_record_buffer_transfer(conn->vhost_stats, output_len);
}
connection_do_write(pair);
connection_modify_epoll(pair->fd, EPOLLIN | EPOLLOUT);
}
@@ -1096,6 +1128,9 @@ static void handle_ssl_handshake(connection_t *conn) {
time_t elapsed = time(NULL) - conn->ssl_handshake_start;
if (elapsed > SSL_HANDSHAKE_TIMEOUT_SEC) {
log_debug("SSL handshake timeout for fd %d after %ld seconds", conn->fd, (long)elapsed);
if (conn->vhost_stats) {
monitor_record_error(conn->vhost_stats, ERROR_TYPE_TIMEOUT);
}
if (conn->pair) {
connection_send_error_response(conn->pair, 504, "Gateway Timeout", "SSL handshake timeout");
}
@@ -1144,6 +1179,9 @@ static void handle_ssl_handshake(connection_t *conn) {
connection_modify_epoll(conn->fd, EPOLLOUT);
} else {
log_debug("SSL handshake failed for fd %d: %d", conn->fd, ssl_error);
if (conn->vhost_stats) {
monitor_record_error(conn->vhost_stats, ERROR_TYPE_SSL);
}
if (conn->pair) {
connection_send_error_response(conn->pair, 502, "Bad Gateway", "SSL handshake failed");
} else {
@@ -1218,6 +1256,9 @@ void connection_handle_event(struct epoll_event *event) {
if (event->events & (EPOLLERR | EPOLLHUP)) {
if (event->events & EPOLLERR) {
log_debug("EPOLLERR on fd %d", fd);
if (conn->vhost_stats) {
monitor_record_error(conn->vhost_stats, ERROR_TYPE_CONNECTION);
}
}
connection_close(fd);
return;
@@ -1267,6 +1308,9 @@ void connection_cleanup_idle(void) {
conn->fd != -1) {
if (current_time - conn->last_activity > CONNECTION_TIMEOUT) {
log_debug("Closing idle connection fd=%d", i);
if (conn->vhost_stats) {
monitor_record_error(conn->vhost_stats, ERROR_TYPE_TIMEOUT);
}
connection_close(i);
}
}
+210 -257
View File
@@ -19,275 +19,120 @@ static const char *DASHBOARD_HTML =
" <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"
" .header { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); margin-bottom: 30px; gap: 15px; }\n"
" .metric { text-align: center; background: #111; border-radius: 8px; padding: 12px; }\n"
" .metric-value { font-size: 28px; font-weight: 300; }\n"
" .metric-label { font-size: 11px; opacity: 0.7; text-transform: uppercase; margin-top: 4px; }\n"
" .metric-value.health { color: #2ecc71; }\n"
" .metric-value.warning { color: #f39c12; }\n"
" .metric-value.danger { color: #e74c3c; }\n"
" .charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; margin-bottom: 20px; }\n"
" .chart-container { background: #111; border-radius: 8px; padding: 20px; height: 220px; position: relative; }\n"
" .chart-container.tall { height: 280px; }\n"
" .chart-title { position: absolute; top: 10px; left: 20px; font-size: 13px; 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"
" .process-table { background: #111; border-radius: 8px; padding: 20px; overflow-x: auto; }\n"
" table { width: 100%; border-collapse: collapse; font-size: 13px; }\n"
" th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid #2a2e3e; }\n"
" th { font-weight: 500; opacity: 0.7; white-space: nowrap; }\n"
" .legend { position: absolute; top: 10px; right: 20px; display: flex; gap: 15px; font-size: 11px; z-index: 10; }\n"
" .legend-item { display: flex; align-items: center; gap: 4px; }\n"
" .legend-color { width: 10px; height: 10px; border-radius: 2px; }\n"
" .vhost-chart-container { height: 180px; background: #111; border-radius: 8px; position: relative; padding: 15px; margin-bottom: 10px; }\n"
" .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }\n"
" .status-ok { background: #2ecc71; }\n"
" .status-warn { background: #f39c12; }\n"
" .status-error { background: #e74c3c; }\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 class=\"metric\"><div class=\"metric-value health\" id=\"healthScore\">100</div><div class=\"metric-label\">Health</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"rps\">0</div><div class=\"metric-label\">Req/sec</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"p50\">0</div><div class=\"metric-label\">p50 ms</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"p99\">0</div><div class=\"metric-label\">p99 ms</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"errorRate\">0%</div><div class=\"metric-label\">Errors</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"connections\">0</div><div class=\"metric-label\">Conns</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"cpu\">0</div><div class=\"metric-label\">CPU</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"memory\">0</div><div class=\"metric-label\">Memory</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"uptime\">0s</div><div class=\"metric-label\">Uptime</div></div>\n"
" </div>\n"
"\n"
" <div class=\"chart-container\">\n"
" <div class=\"chart-title\">CPU Usage</div>\n"
" <canvas id=\"cpuChart\"></canvas>\n"
" <div class=\"charts-grid\">\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Latency Distribution</div><canvas id=\"latencyHistChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Status Codes</div><canvas id=\"statusChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">HTTP Methods</div><canvas id=\"methodsChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Efficiency</div><canvas id=\"efficiencyChart\"></canvas></div>\n"
" </div>\n"
"\n"
" <div class=\"chart-container\">\n"
" <div class=\"chart-title\">Memory Usage</div>\n"
" <canvas id=\"memChart\"></canvas>\n"
" <div class=\"charts-grid\">\n"
" <div class=\"chart-container\"><div class=\"chart-title\">CPU Usage</div><canvas id=\"cpuChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Memory Usage</div><canvas id=\"memChart\"></canvas></div>\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 class=\"charts-grid\">\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Network I/O</div>\n"
" <div class=\"legend\"><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#3498db\"></div><span>RX</span></div><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#2ecc71\"></div><span>TX</span></div></div>\n"
" <canvas id=\"netChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Disk I/O</div>\n"
" <div class=\"legend\"><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#9b59b6\"></div><span>Read</span></div><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#e67e22\"></div><span>Write</span></div></div>\n"
" <canvas id=\"diskChart\"></canvas></div>\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"
" <div class=\"chart-container tall\" style=\"margin-bottom:20px;\"><div class=\"chart-title\">Load Average</div>\n"
" <div class=\"legend\"><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#e74c3c\"></div><span>1m</span></div><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#f39c12\"></div><span>5m</span></div><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#3498db\"></div><span>15m</span></div></div>\n"
" <canvas id=\"loadChart\"></canvas></div>\n"
" <div class=\"process-table\"><table><thead><tr><th>Virtual Host</th><th>RPS</th><th>Total</th><th>p50</th><th>p99</th><th>2xx</th><th>4xx</th><th>5xx</th><th>Err%</th><th>Sent</th><th>Recv</th></tr></thead><tbody id=\"processTable\"></tbody></table></div>\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"
" const formatTime = v => { const s=Math.abs(Math.round(v/1000)); if(s===0)return'now'; const m=Math.floor(s/60),ss=s%60; return`-${m>0?m+'m ':''}${ss}s`; };\n"
" const formatSize = v => { if(v>=1048576)return(v/1048576).toFixed(1)+' GB/s'; if(v>=1024)return(v/1024).toFixed(1)+' MB/s'; return v.toFixed(0)+' KB/s'; };\n"
" const formatDisk = v => v>=1024?(v/1024).toFixed(1)+' GB/s':v.toFixed(1)+' MB/s';\n"
" const formatBytes = b => { if(b===0)return'0 B'; const k=1024,s=['B','KB','MB','GB','TB'],i=Math.floor(Math.log(b)/Math.log(k)); return(b/Math.pow(k,i)).toFixed(1)+' '+s[i]; };\n"
" const formatUptime = s => { const d=Math.floor(s/86400),h=Math.floor((s%86400)/3600),m=Math.floor((s%3600)/60); return d>0?d+'d '+h+'h':h>0?h+'h '+m+'m':m+'m'; };\n"
" const baseOpts = (sec,yCb) => ({responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},interaction:{mode:'nearest',axis:'x',intersect:false},scales:{x:{type:'linear',display:true,grid:{color:'#2a2e3e'},ticks:{color:'#666',maxTicksLimit:7,callback:formatTime},min:-sec*1000,max:0},y:{display:true,grid:{color:'#2a2e3e'},ticks:{color:'#666',beginAtZero:true,callback:yCb}}},plugins:{legend:{display:false}},elements:{point:{radius:0},line:{borderWidth:2,tension:0.4,fill:true}}});\n"
" const cpuChart = new Chart(document.getElementById('cpuChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#f39c12',backgroundColor:'rgba(243,156,18,0.1)'}]},options:{...baseOpts(300,v=>v+'%'),scales:{...baseOpts(300).scales,y:{...baseOpts(300).scales.y,max:100}}}});\n"
" const memChart = new Chart(document.getElementById('memChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#e74c3c',backgroundColor:'rgba(231,76,60,0.1)'}]},options:baseOpts(300,v=>v.toFixed(1)+' GB')});\n"
" const netChart = new Chart(document.getElementById('netChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#3498db',backgroundColor:'rgba(52,152,219,0.1)'},{data:[],borderColor:'#2ecc71',backgroundColor:'rgba(46,204,113,0.1)'}]},options:baseOpts(300,formatSize)});\n"
" const diskChart = new Chart(document.getElementById('diskChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#9b59b6',backgroundColor:'rgba(155,89,182,0.1)'},{data:[],borderColor:'#e67e22',backgroundColor:'rgba(230,126,34,0.1)'}]},options:baseOpts(300,formatDisk)});\n"
" const loadChart = new Chart(document.getElementById('loadChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#e74c3c',backgroundColor:'rgba(231,76,60,0.1)'},{data:[],borderColor:'#f39c12',backgroundColor:'rgba(243,156,18,0.1)'},{data:[],borderColor:'#3498db',backgroundColor:'rgba(52,152,219,0.1)'}]},options:baseOpts(300,v=>v.toFixed(2))});\n"
" const latencyHistChart = new Chart(document.getElementById('latencyHistChart'),{type:'bar',data:{labels:[],datasets:[{data:[],backgroundColor:v=>{const i=v.dataIndex;return i<4?'#2ecc71':i<8?'#f39c12':'#e74c3c';}}]},options:{responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},plugins:{legend:{display:false}},scales:{x:{grid:{display:false},ticks:{color:'#666',maxRotation:45}},y:{grid:{color:'#2a2e3e'},ticks:{color:'#666'}}}}});\n"
" const statusChart = new Chart(document.getElementById('statusChart'),{type:'doughnut',data:{labels:['2xx','3xx','4xx','5xx'],datasets:[{data:[0,0,0,0],backgroundColor:['#2ecc71','#3498db','#f39c12','#e74c3c']}]},options:{responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},plugins:{legend:{display:true,position:'right',labels:{color:'#fff',font:{size:11}}}}}});\n"
" const methodsChart = new Chart(document.getElementById('methodsChart'),{type:'bar',data:{labels:['GET','POST','PUT','DEL','PATCH','HEAD','OPT'],datasets:[{data:[0,0,0,0,0,0,0],backgroundColor:'#3498db'}]},options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},plugins:{legend:{display:false}},scales:{x:{grid:{color:'#2a2e3e'},ticks:{color:'#666'}},y:{grid:{display:false},ticks:{color:'#666'}}}}});\n"
" const efficiencyChart = new Chart(document.getElementById('efficiencyChart'),{type:'doughnut',data:{labels:['Zero-Copy','Buffered'],datasets:[{data:[0,0],backgroundColor:['#2ecc71','#9b59b6']}]},options:{responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},plugins:{legend:{display:true,position:'right',labels:{color:'#fff',font:{size:11}}}}}});\n"
" window.vhostCharts={}; let prevNames=[];\n"
" async function updateStats(){\n"
" try{\n"
" const r=await fetch('/rproxy/api/stats'),d=await r.json();\n"
" const hs=d.current.health_score||100; document.getElementById('healthScore').textContent=hs.toFixed(0);\n"
" const hEl=document.getElementById('healthScore'); hEl.className='metric-value '+(hs>=80?'health':hs>=50?'warning':'danger');\n"
" document.getElementById('rps').textContent=(d.current.requests_per_second||0).toFixed(0);\n"
" document.getElementById('p50').textContent=(d.latency?.p50_ms||0).toFixed(0);\n"
" document.getElementById('p99').textContent=(d.latency?.p99_ms||0).toFixed(0);\n"
" document.getElementById('errorRate').textContent=((d.current.error_rate_1m||0)*100).toFixed(1)+'%';\n"
" document.getElementById('connections').textContent=d.current.active_connections;\n"
" document.getElementById('cpu').textContent=d.current.cpu_percent+'%';\n"
" document.getElementById('memory').textContent=d.current.memory_gb+'G';\n"
" document.getElementById('uptime').textContent=formatUptime(d.current.uptime_seconds||0);\n"
" cpuChart.data.datasets[0].data=d.cpu_history; memChart.data.datasets[0].data=d.memory_history;\n"
" netChart.data.datasets[0].data=d.network_rx_history; netChart.data.datasets[1].data=d.network_tx_history;\n"
" diskChart.data.datasets[0].data=d.disk_read_history; diskChart.data.datasets[1].data=d.disk_write_history;\n"
" loadChart.data.datasets[0].data=d.load1_history; loadChart.data.datasets[1].data=d.load5_history; loadChart.data.datasets[2].data=d.load15_history;\n"
" [cpuChart,memChart,netChart,diskChart,loadChart].forEach(c=>c.update('none'));\n"
" if(d.latency?.histogram){latencyHistChart.data.labels=d.latency.bucket_labels||[]; latencyHistChart.data.datasets[0].data=d.latency.histogram; latencyHistChart.update('none');}\n"
" if(d.status_codes){statusChart.data.datasets[0].data=[d.status_codes['2xx']||0,d.status_codes['3xx']||0,d.status_codes['4xx']||0,d.status_codes['5xx']||0]; statusChart.update('none');}\n"
" if(d.methods){methodsChart.data.datasets[0].data=[d.methods.GET||0,d.methods.POST||0,d.methods.PUT||0,d.methods.DELETE||0,d.methods.PATCH||0,d.methods.HEAD||0,d.methods.OPTIONS||0]; methodsChart.update('none');}\n"
" if(d.efficiency){efficiencyChart.data.datasets[0].data=[d.efficiency.splice_transfers||0,d.efficiency.buffer_transfers||0]; efficiencyChart.update('none');}\n"
" const tbody=document.getElementById('processTable'),names=d.processes.map(p=>p.name).join(',');\n"
" if(names!==prevNames.join(',')){ Object.values(window.vhostCharts).forEach(c=>c.destroy()); window.vhostCharts={}; tbody.innerHTML=''; prevNames=d.processes.map(p=>p.name); }\n"
" d.processes.forEach((p,i)=>{\n"
" let row=tbody.children[i*2]; if(!row){row=document.createElement('tr'); tbody.appendChild(row);}\n"
" const errP=(p.error_rate*100).toFixed(1); const cls=parseFloat(errP)>5?'status-error':parseFloat(errP)>1?'status-warn':'status-ok';\n"
" row.innerHTML=`<td><span class='status-indicator ${cls}'></span>${p.name}</td><td>${p.rps||0}</td><td>${p.total_requests}</td><td>${(p.latency_p50||0).toFixed(0)}</td><td>${(p.latency_p99||0).toFixed(0)}</td><td>${p.status_2xx||0}</td><td>${p.status_4xx||0}</td><td>${p.status_5xx||0}</td><td>${errP}%</td><td>${formatBytes(p.bytes_sent)}</td><td>${formatBytes(p.bytes_recv)}</td>`;\n"
" let chartRow=tbody.children[i*2+1]; if(!chartRow){chartRow=document.createElement('tr'); chartRow.innerHTML=`<td colspan='11' style='padding:10px 0;border:none;'><div class='vhost-chart-container'><div class='chart-title'>Throughput - ${p.name}</div><canvas id='vc${i}'></canvas></div></td>`; tbody.appendChild(chartRow);}\n"
" const cid='vc'+i,cv=document.getElementById(cid); if(!cv)return;\n"
" if(!window.vhostCharts[cid]){const cols=[{b:'#3498db',bg:'rgba(52,152,219,0.1)'},{b:'#2ecc71',bg:'rgba(46,204,113,0.1)'},{b:'#f39c12',bg:'rgba(243,156,18,0.1)'},{b:'#e74c3c',bg:'rgba(231,76,60,0.1)'}],c=cols[i%4]; window.vhostCharts[cid]=new Chart(cv,{type:'line',data:{datasets:[{data:p.throughput_history||[],borderColor:c.b,backgroundColor:c.bg}]},options:baseOpts(60,formatSize)});}\n"
" else{window.vhostCharts[cid].data.datasets[0].data=p.throughput_history||[]; window.vhostCharts[cid].update('none');}\n"
" });\n"
" }catch(e){console.error('Stats error:',e);}\n"
" }\n"
"\n"
" updateStats();\n"
" setInterval(updateStats, 1000);\n"
" updateStats(); setInterval(updateStats,1000);\n"
" </script>\n"
"</body>\n"
"</html>\n";
@@ -497,6 +342,91 @@ void dashboard_serve_stats_api(connection_t *conn, const char *request_data, siz
cJSON_AddNumberToObject(current, "load_5m", load5);
cJSON_AddNumberToObject(current, "load_15m", load15);
monitor_compute_health_score();
double current_rps = monitor_get_current_rps();
cJSON_AddNumberToObject(current, "health_score", monitor.health_score);
cJSON_AddNumberToObject(current, "requests_per_second", current_rps);
cJSON_AddNumberToObject(current, "error_rate_1m", monitor.error_rate_1m);
cJSON_AddNumberToObject(current, "uptime_seconds", (double)(time(NULL) - monitor.uptime_start));
cJSON_AddNumberToObject(current, "peak_rps", monitor.peak_rps);
cJSON_AddNumberToObject(current, "total_connections", (double)monitor.total_connections_accepted);
cJSON *latency = cJSON_CreateObject();
if (latency) {
cJSON_AddNumberToObject(latency, "p50_ms", histogram_percentile(&monitor.global_latency, 0.50));
cJSON_AddNumberToObject(latency, "p90_ms", histogram_percentile(&monitor.global_latency, 0.90));
cJSON_AddNumberToObject(latency, "p95_ms", histogram_percentile(&monitor.global_latency, 0.95));
cJSON_AddNumberToObject(latency, "p99_ms", histogram_percentile(&monitor.global_latency, 0.99));
cJSON_AddNumberToObject(latency, "mean_ms", histogram_mean(&monitor.global_latency));
cJSON *histogram_arr = cJSON_CreateArray();
if (histogram_arr) {
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
cJSON_AddItemToArray(histogram_arr, cJSON_CreateNumber(monitor.global_latency.buckets[i]));
}
cJSON_AddItemToObject(latency, "histogram", histogram_arr);
}
cJSON *labels = cJSON_CreateArray();
if (labels) {
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
cJSON_AddItemToArray(labels, cJSON_CreateString(LATENCY_BUCKET_LABELS[i]));
}
cJSON_AddItemToObject(latency, "bucket_labels", labels);
}
cJSON_AddItemToObject(root, "latency", latency);
}
uint64_t total_2xx = 0, total_3xx = 0, total_4xx = 0, total_5xx = 0;
uint64_t methods[HTTP_METHOD_COUNT] = {0};
uint64_t total_splice = 0, total_buffer = 0;
uint64_t total_splice_bytes = 0, total_buffer_bytes = 0;
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
total_2xx += s->status_counts.status_2xx;
total_3xx += s->status_counts.status_3xx;
total_4xx += s->status_counts.status_4xx;
total_5xx += s->status_counts.status_5xx;
for (int i = 0; i < HTTP_METHOD_COUNT; i++) {
methods[i] += s->method_counts.counts[i];
}
total_splice += s->splice_transfers;
total_buffer += s->buffered_transfers;
total_splice_bytes += s->bytes_via_splice;
total_buffer_bytes += s->bytes_via_buffer;
}
cJSON *status_codes = cJSON_CreateObject();
if (status_codes) {
cJSON_AddNumberToObject(status_codes, "2xx", (double)total_2xx);
cJSON_AddNumberToObject(status_codes, "3xx", (double)total_3xx);
cJSON_AddNumberToObject(status_codes, "4xx", (double)total_4xx);
cJSON_AddNumberToObject(status_codes, "5xx", (double)total_5xx);
cJSON_AddItemToObject(root, "status_codes", status_codes);
}
cJSON *methods_obj = cJSON_CreateObject();
if (methods_obj) {
cJSON_AddNumberToObject(methods_obj, "GET", (double)methods[HTTP_METHOD_GET]);
cJSON_AddNumberToObject(methods_obj, "POST", (double)methods[HTTP_METHOD_POST]);
cJSON_AddNumberToObject(methods_obj, "PUT", (double)methods[HTTP_METHOD_PUT]);
cJSON_AddNumberToObject(methods_obj, "DELETE", (double)methods[HTTP_METHOD_DELETE]);
cJSON_AddNumberToObject(methods_obj, "PATCH", (double)methods[HTTP_METHOD_PATCH]);
cJSON_AddNumberToObject(methods_obj, "HEAD", (double)methods[HTTP_METHOD_HEAD]);
cJSON_AddNumberToObject(methods_obj, "OPTIONS", (double)methods[HTTP_METHOD_OPTIONS]);
cJSON_AddNumberToObject(methods_obj, "OTHER", (double)methods[HTTP_METHOD_OTHER]);
cJSON_AddItemToObject(root, "methods", methods_obj);
}
cJSON *efficiency = cJSON_CreateObject();
if (efficiency) {
double total_transfers = (double)(total_splice + total_buffer);
double splice_ratio = total_transfers > 0 ? (double)total_splice / total_transfers : 0;
cJSON_AddNumberToObject(efficiency, "zero_copy_ratio", splice_ratio);
cJSON_AddNumberToObject(efficiency, "splice_transfers", (double)total_splice);
cJSON_AddNumberToObject(efficiency, "buffer_transfers", (double)total_buffer);
cJSON_AddNumberToObject(efficiency, "bytes_via_splice", (double)total_splice_bytes);
cJSON_AddNumberToObject(efficiency, "bytes_via_buffer", (double)total_buffer_bytes);
cJSON_AddItemToObject(root, "efficiency", efficiency);
}
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"));
@@ -522,6 +452,29 @@ void dashboard_serve_stats_api(connection_t *conn, const char *request_data, siz
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_AddNumberToObject(p, "latency_p50", histogram_percentile(&s->latency_histogram, 0.50));
cJSON_AddNumberToObject(p, "latency_p90", histogram_percentile(&s->latency_histogram, 0.90));
cJSON_AddNumberToObject(p, "latency_p95", histogram_percentile(&s->latency_histogram, 0.95));
cJSON_AddNumberToObject(p, "latency_p99", histogram_percentile(&s->latency_histogram, 0.99));
cJSON_AddNumberToObject(p, "rps", rate_tracker_get_rps(&s->requests_per_second));
double vhost_total = (double)(s->status_counts.status_2xx + s->status_counts.status_3xx +
s->status_counts.status_4xx + s->status_counts.status_5xx);
double error_rate = vhost_total > 0 ? (double)s->status_counts.status_5xx / vhost_total : 0;
cJSON_AddNumberToObject(p, "error_rate", error_rate);
cJSON_AddNumberToObject(p, "status_2xx", (double)s->status_counts.status_2xx);
cJSON_AddNumberToObject(p, "status_3xx", (double)s->status_counts.status_3xx);
cJSON_AddNumberToObject(p, "status_4xx", (double)s->status_counts.status_4xx);
cJSON_AddNumberToObject(p, "status_5xx", (double)s->status_counts.status_5xx);
cJSON_AddNumberToObject(p, "upstream_success", (double)s->upstream_connect_success);
cJSON_AddNumberToObject(p, "upstream_failures", (double)s->upstream_connect_failures);
cJSON_AddNumberToObject(p, "dns_failures", (double)s->dns_failures);
cJSON_AddNumberToObject(p, "ssl_failures", (double)s->ssl_failures);
cJSON_AddNumberToObject(p, "timeout_errors", (double)s->timeout_errors);
cJSON_AddItemToArray(processes, p);
}
}
+25
View File
@@ -283,3 +283,28 @@ int http_find_header_line_bounds(const char* data, size_t len, const char* name,
*line_end = NULL;
return 0;
}
int http_extract_status_code(const char *data, size_t len) {
if (!data || len < 12) return 0;
if (strncmp(data, "HTTP/1.", 7) != 0 && strncmp(data, "HTTP/2", 6) != 0) {
return 0;
}
const char *p = data;
while (p < data + len && *p != ' ') p++;
if (p >= data + len) return 0;
while (p < data + len && *p == ' ') p++;
if (p >= data + len) return 0;
int status = 0;
for (int i = 0; i < 3 && p + i < data + len; i++) {
if (p[i] >= '0' && p[i] <= '9') {
status = status * 10 + (p[i] - '0');
} else {
break;
}
}
return (status >= 100 && status < 600) ? status : 0;
}
+1
View File
@@ -12,5 +12,6 @@ long http_get_content_length(const char *headers, size_t headers_len);
int http_find_headers_end(const char *data, size_t len, size_t *headers_end);
int http_rewrite_content_length(char *headers, size_t *headers_len, size_t max_len, long new_length);
int http_find_header_line_bounds(const char* data, size_t len, const char* name, const char** line_start, const char** line_end);
int http_extract_status_code(const char *data, size_t len);
#endif
+1 -1
View File
@@ -32,7 +32,7 @@ static void rotate_log_file(void) {
g_log_file = NULL;
}
char old_path[520], new_path[520];
char old_path[536], new_path[536];
snprintf(old_path, sizeof(old_path), "%s.%d", g_log_path, LOG_MAX_ROTATIONS);
unlink(old_path);
+293
View File
@@ -151,6 +151,8 @@ static void load_stats_from_db(void) {
void monitor_init(const char *db_file) {
memset(&monitor, 0, sizeof(system_monitor_t));
monitor.start_time = time(NULL);
monitor.uptime_start = time(NULL);
monitor.health_score = 100.0;
history_deque_init(&monitor.cpu_history, HISTORY_SECONDS);
history_deque_init(&monitor.memory_history, HISTORY_SECONDS);
@@ -161,6 +163,10 @@ void monitor_init(const char *db_file) {
history_deque_init(&monitor.load5_history, HISTORY_SECONDS);
history_deque_init(&monitor.load15_history, HISTORY_SECONDS);
histogram_init(&monitor.global_latency);
histogram_init(&monitor.connection_lifetime);
rate_tracker_init(&monitor.global_rps);
if (sqlite3_open(db_file, &monitor.db) != SQLITE_OK) {
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(monitor.db));
if (monitor.db) {
@@ -487,6 +493,14 @@ vhost_stats_t* monitor_get_or_create_vhost_stats(const char *vhost_name) {
new_stats->last_update = time(NULL);
history_deque_init(&new_stats->throughput_history, 60);
request_time_deque_init(&new_stats->request_times, 100);
histogram_init(&new_stats->latency_histogram);
histogram_init(&new_stats->request_size_histogram);
histogram_init(&new_stats->response_size_histogram);
histogram_init(&new_stats->ttfb_histogram);
histogram_init(&new_stats->upstream_connect_latency);
rate_tracker_init(&new_stats->requests_per_second);
new_stats->next = monitor.vhost_stats_head;
monitor.vhost_stats_head = new_stats;
@@ -501,6 +515,8 @@ void monitor_record_request_start(vhost_stats_t *stats, int is_websocket) {
stats->http_requests++;
}
stats->total_requests++;
rate_tracker_increment(&stats->requests_per_second);
rate_tracker_increment(&monitor.global_rps);
}
void monitor_record_request_end(vhost_stats_t *stats, double start_time) {
@@ -510,6 +526,8 @@ void monitor_record_request_end(vhost_stats_t *stats, double start_time) {
double duration_ms = ((end_time.tv_sec + end_time.tv_nsec / 1e9) - start_time) * 1000.0;
if (duration_ms >= 0 && duration_ms < 60000) {
request_time_deque_push(&stats->request_times, duration_ms);
histogram_add(&stats->latency_histogram, duration_ms);
histogram_add(&monitor.global_latency, duration_ms);
}
}
@@ -518,3 +536,278 @@ void monitor_record_bytes(vhost_stats_t *stats, long long sent, long long recv)
stats->bytes_sent += sent;
stats->bytes_recv += recv;
}
const double LATENCY_BUCKET_BOUNDS[HISTOGRAM_BUCKETS] = {
1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0,
500.0, 1000.0, 2500.0, 5000.0, 10000.0, 30000.0, 60000.0, 1e9
};
const char* LATENCY_BUCKET_LABELS[HISTOGRAM_BUCKETS] = {
"0-1ms", "1-2ms", "2-5ms", "5-10ms", "10-25ms", "25-50ms", "50-100ms", "100-250ms",
"250-500ms", "500ms-1s", "1-2.5s", "2.5-5s", "5-10s", "10-30s", "30-60s", "60s+"
};
const double SIZE_BUCKET_BOUNDS[HISTOGRAM_BUCKETS] = {
128.0, 512.0, 1024.0, 4096.0, 16384.0, 65536.0, 262144.0, 1048576.0,
4194304.0, 16777216.0, 67108864.0, 268435456.0, 1073741824.0, 4294967296.0, 1e15, 1e18
};
void histogram_init(histogram_t *h) {
if (!h) return;
memset(h, 0, sizeof(histogram_t));
h->min_value = 1e18;
h->max_value = -1e18;
}
void histogram_add(histogram_t *h, double value) {
if (!h) return;
h->total_count++;
h->sum += value;
if (value < h->min_value) h->min_value = value;
if (value > h->max_value) h->max_value = value;
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
if (value <= LATENCY_BUCKET_BOUNDS[i]) {
h->buckets[i]++;
return;
}
}
h->overflow++;
}
static void histogram_add_with_bounds(histogram_t *h, double value, const double *bounds) {
if (!h) return;
h->total_count++;
h->sum += value;
if (value < h->min_value) h->min_value = value;
if (value > h->max_value) h->max_value = value;
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
if (value <= bounds[i]) {
h->buckets[i]++;
return;
}
}
h->overflow++;
}
double histogram_percentile(histogram_t *h, double p) {
if (!h || h->total_count == 0) return 0.0;
uint64_t target = (uint64_t)(h->total_count * p);
uint64_t cumulative = 0;
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
cumulative += h->buckets[i];
if (cumulative >= target) {
return LATENCY_BUCKET_BOUNDS[i];
}
}
return LATENCY_BUCKET_BOUNDS[HISTOGRAM_BUCKETS - 1];
}
double histogram_mean(histogram_t *h) {
if (!h || h->total_count == 0) return 0.0;
return h->sum / h->total_count;
}
void rate_tracker_init(rate_tracker_t *rt) {
if (!rt) return;
memset(rt, 0, sizeof(rate_tracker_t));
rt->slot_start = time(NULL);
}
void rate_tracker_increment(rate_tracker_t *rt) {
if (!rt) return;
time_t now = time(NULL);
int slot = now % RATE_TRACKER_SLOTS;
if (now != rt->slot_start) {
int slots_to_clear = (int)(now - rt->slot_start);
if (slots_to_clear >= RATE_TRACKER_SLOTS) {
memset(rt->counts, 0, sizeof(rt->counts));
} else {
for (int i = 1; i <= slots_to_clear; i++) {
int clear_slot = (rt->current_slot + i) % RATE_TRACKER_SLOTS;
rt->counts[clear_slot] = 0;
}
}
rt->current_slot = slot;
rt->slot_start = now;
}
rt->counts[slot]++;
}
uint32_t rate_tracker_get_rps(rate_tracker_t *rt) {
if (!rt) return 0;
time_t now = time(NULL);
int prev_slot = (now - 1) % RATE_TRACKER_SLOTS;
if ((now - rt->slot_start) > RATE_TRACKER_SLOTS) return 0;
return rt->counts[prev_slot];
}
uint32_t rate_tracker_get_total_last_minute(rate_tracker_t *rt) {
if (!rt) return 0;
time_t now = time(NULL);
if ((now - rt->slot_start) > RATE_TRACKER_SLOTS) {
return 0;
}
uint32_t total = 0;
for (int i = 0; i < RATE_TRACKER_SLOTS; i++) {
total += rt->counts[i];
}
return total;
}
http_method_t http_method_from_string(const char *method) {
if (!method) return HTTP_METHOD_OTHER;
if (strcmp(method, "GET") == 0) return HTTP_METHOD_GET;
if (strcmp(method, "POST") == 0) return HTTP_METHOD_POST;
if (strcmp(method, "PUT") == 0) return HTTP_METHOD_PUT;
if (strcmp(method, "DELETE") == 0) return HTTP_METHOD_DELETE;
if (strcmp(method, "PATCH") == 0) return HTTP_METHOD_PATCH;
if (strcmp(method, "HEAD") == 0) return HTTP_METHOD_HEAD;
if (strcmp(method, "OPTIONS") == 0) return HTTP_METHOD_OPTIONS;
return HTTP_METHOD_OTHER;
}
void monitor_record_method(vhost_stats_t *stats, http_method_t method) {
if (!stats || method >= HTTP_METHOD_COUNT) return;
stats->method_counts.counts[method]++;
}
void monitor_record_status(vhost_stats_t *stats, int status_code) {
if (!stats) return;
if (status_code >= 100 && status_code < 200) {
stats->status_counts.status_1xx++;
} else if (status_code >= 200 && status_code < 300) {
stats->status_counts.status_2xx++;
} else if (status_code >= 300 && status_code < 400) {
stats->status_counts.status_3xx++;
} else if (status_code >= 400 && status_code < 500) {
stats->status_counts.status_4xx++;
} else if (status_code >= 500 && status_code < 600) {
stats->status_counts.status_5xx++;
} else {
stats->status_counts.status_unknown++;
}
}
void monitor_record_request_size(vhost_stats_t *stats, long size) {
if (!stats || size < 0) return;
histogram_add_with_bounds(&stats->request_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
}
void monitor_record_response_size(vhost_stats_t *stats, long size) {
if (!stats || size < 0) return;
histogram_add_with_bounds(&stats->response_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
stats->response_bytes_total += size;
}
void monitor_record_ttfb(vhost_stats_t *stats, double ttfb_ms) {
if (!stats || ttfb_ms < 0) return;
histogram_add(&stats->ttfb_histogram, ttfb_ms);
}
void monitor_record_upstream_connect(vhost_stats_t *stats, int success, double latency_ms) {
if (!stats) return;
if (success) {
stats->upstream_connect_success++;
if (latency_ms >= 0) {
histogram_add(&stats->upstream_connect_latency, latency_ms);
}
} else {
stats->upstream_connect_failures++;
}
}
void monitor_record_splice_transfer(vhost_stats_t *stats, long long bytes) {
if (!stats) return;
stats->splice_transfers++;
stats->bytes_via_splice += bytes;
}
void monitor_record_buffer_transfer(vhost_stats_t *stats, long long bytes) {
if (!stats) return;
stats->buffered_transfers++;
stats->bytes_via_buffer += bytes;
}
void monitor_record_connection_opened(vhost_stats_t *stats) {
if (!stats) return;
stats->connections_opened++;
monitor.total_connections_accepted++;
}
void monitor_record_connection_closed(vhost_stats_t *stats) {
if (!stats) return;
stats->connections_closed++;
}
void monitor_record_keepalive_reuse(vhost_stats_t *stats) {
if (!stats) return;
stats->keep_alive_reused++;
}
void monitor_record_error(vhost_stats_t *stats, int error_type) {
if (!stats) return;
switch (error_type) {
case ERROR_TYPE_DNS:
stats->dns_failures++;
break;
case ERROR_TYPE_SSL:
stats->ssl_failures++;
break;
case ERROR_TYPE_TIMEOUT:
stats->timeout_errors++;
break;
case ERROR_TYPE_CONNECTION:
stats->connection_errors++;
break;
}
}
void monitor_compute_health_score(void) {
uint64_t total_requests = 0;
uint64_t total_errors = 0;
uint64_t total_upstream_failures = 0;
uint64_t total_upstream_attempts = 0;
uint64_t total_timeouts = 0;
for (vhost_stats_t *s = monitor.vhost_stats_head; s != NULL; s = s->next) {
total_requests += s->total_requests;
total_errors += s->status_counts.status_5xx;
total_upstream_failures += s->upstream_connect_failures;
total_upstream_attempts += s->upstream_connect_success + s->upstream_connect_failures;
total_timeouts += s->timeout_errors;
}
double error_rate = total_requests > 0 ? (double)total_errors / total_requests : 0;
double upstream_fail_rate = total_upstream_attempts > 0 ? (double)total_upstream_failures / total_upstream_attempts : 0;
double timeout_rate = total_requests > 0 ? (double)total_timeouts / total_requests : 0;
monitor.health_score = 100.0 * (1.0 - (error_rate * 0.5 + upstream_fail_rate * 0.3 + timeout_rate * 0.2));
if (monitor.health_score < 0) monitor.health_score = 0;
if (monitor.health_score > 100) monitor.health_score = 100;
monitor.error_rate_1m = error_rate;
}
void monitor_update_connection_states(void) {
memset(monitor.connections_by_state, 0, sizeof(monitor.connections_by_state));
}
double monitor_get_current_rps(void) {
uint32_t total_rps = 0;
for (vhost_stats_t *s = monitor.vhost_stats_head; s != NULL; s = s->next) {
total_rps += rate_tracker_get_rps(&s->requests_per_second);
}
double rps = (double)total_rps;
if (rps > monitor.peak_rps) {
monitor.peak_rps = rps;
monitor.peak_rps_time = time(NULL);
}
return rps;
}
+37
View File
@@ -22,4 +22,41 @@ void disk_history_deque_push(disk_history_deque_t *dq, double time, double read_
void request_time_deque_init(request_time_deque_t *dq, int capacity);
void request_time_deque_push(request_time_deque_t *dq, double time_ms);
void histogram_init(histogram_t *h);
void histogram_add(histogram_t *h, double value);
double histogram_percentile(histogram_t *h, double p);
double histogram_mean(histogram_t *h);
void rate_tracker_init(rate_tracker_t *rt);
void rate_tracker_increment(rate_tracker_t *rt);
uint32_t rate_tracker_get_rps(rate_tracker_t *rt);
uint32_t rate_tracker_get_total_last_minute(rate_tracker_t *rt);
http_method_t http_method_from_string(const char *method);
void monitor_record_method(vhost_stats_t *stats, http_method_t method);
void monitor_record_status(vhost_stats_t *stats, int status_code);
void monitor_record_request_size(vhost_stats_t *stats, long size);
void monitor_record_response_size(vhost_stats_t *stats, long size);
void monitor_record_ttfb(vhost_stats_t *stats, double ttfb_ms);
void monitor_record_upstream_connect(vhost_stats_t *stats, int success, double latency_ms);
void monitor_record_splice_transfer(vhost_stats_t *stats, long long bytes);
void monitor_record_buffer_transfer(vhost_stats_t *stats, long long bytes);
void monitor_record_connection_opened(vhost_stats_t *stats);
void monitor_record_connection_closed(vhost_stats_t *stats);
void monitor_record_keepalive_reuse(vhost_stats_t *stats);
void monitor_record_error(vhost_stats_t *stats, int error_type);
void monitor_compute_health_score(void);
void monitor_update_connection_states(void);
double monitor_get_current_rps(void);
#define ERROR_TYPE_DNS 0
#define ERROR_TYPE_SSL 1
#define ERROR_TYPE_TIMEOUT 2
#define ERROR_TYPE_CONNECTION 3
extern const double LATENCY_BUCKET_BOUNDS[HISTOGRAM_BUCKETS];
extern const char* LATENCY_BUCKET_LABELS[HISTOGRAM_BUCKETS];
extern const double SIZE_BUCKET_BOUNDS[HISTOGRAM_BUCKETS];
#endif
+95
View File
@@ -35,6 +35,9 @@
#define SSL_HANDSHAKE_TIMEOUT_SEC 10
#define MAX_CONNECTIONS_PER_IP 100
#define HOSTNAME_MAX_LEN 256
#define HISTOGRAM_BUCKETS 16
#define RATE_TRACKER_SLOTS 60
#define HTTP_METHOD_COUNT 8
typedef enum {
CONN_TYPE_UNUSED,
@@ -51,6 +54,45 @@ typedef enum {
CLIENT_STATE_CLOSING
} client_state_t;
typedef enum {
HTTP_METHOD_GET = 0,
HTTP_METHOD_POST,
HTTP_METHOD_PUT,
HTTP_METHOD_DELETE,
HTTP_METHOD_PATCH,
HTTP_METHOD_HEAD,
HTTP_METHOD_OPTIONS,
HTTP_METHOD_OTHER
} http_method_t;
typedef struct {
uint32_t buckets[HISTOGRAM_BUCKETS];
uint32_t overflow;
uint64_t total_count;
double sum;
double min_value;
double max_value;
} histogram_t;
typedef struct {
uint64_t counts[HTTP_METHOD_COUNT];
} method_counter_t;
typedef struct {
uint64_t status_1xx;
uint64_t status_2xx;
uint64_t status_3xx;
uint64_t status_4xx;
uint64_t status_5xx;
uint64_t status_unknown;
} status_counter_t;
typedef struct {
uint32_t counts[RATE_TRACKER_SLOTS];
int current_slot;
time_t slot_start;
} rate_tracker_t;
typedef struct {
char *data;
size_t capacity;
@@ -199,6 +241,43 @@ typedef struct vhost_stats_s {
double last_update;
history_deque_t throughput_history;
request_time_deque_t request_times;
histogram_t latency_histogram;
histogram_t request_size_histogram;
histogram_t response_size_histogram;
histogram_t ttfb_histogram;
histogram_t upstream_connect_latency;
method_counter_t method_counts;
status_counter_t status_counts;
rate_tracker_t requests_per_second;
uint64_t keep_alive_reused;
uint64_t connections_opened;
uint64_t connections_closed;
uint64_t splice_transfers;
uint64_t buffered_transfers;
uint64_t bytes_via_splice;
uint64_t bytes_via_buffer;
uint64_t upstream_connect_success;
uint64_t upstream_connect_failures;
uint64_t upstream_retries;
uint64_t rate_limit_rejections;
uint64_t auth_failures;
uint64_t patch_blocks;
uint64_t dns_failures;
uint64_t ssl_failures;
uint64_t timeout_errors;
uint64_t connection_errors;
double peak_rps;
time_t peak_rps_time;
long long response_bytes_total;
struct vhost_stats_s *next;
} vhost_stats_t;
@@ -221,6 +300,22 @@ typedef struct {
double last_disk_update_time;
vhost_stats_t *vhost_stats_head;
sqlite3 *db;
histogram_t global_latency;
histogram_t connection_lifetime;
rate_tracker_t global_rps;
uint64_t total_connections_accepted;
uint64_t connections_by_state[5];
uint64_t total_rate_limit_checks;
uint64_t total_rate_limit_blocks;
time_t uptime_start;
double health_score;
double error_rate_1m;
double peak_rps;
time_t peak_rps_time;
} system_monitor_t;
#endif