feat: integrate monitoring metrics into connection handling
Some checks failed
Build and Test / build (push) Failing after 39s
Build and Test / coverage (push) Failing after 46s

feat: redesign dashboard with advanced metrics and charts
This commit is contained in:
retoor 2025-12-29 01:37:24 +01:00
parent 6ad8586770
commit c414e2c2b1
10 changed files with 993 additions and 260 deletions

View File

@ -6,6 +6,14 @@
## Version 0.6.0 - 2025-12-29
Integrates monitoring metrics into connection handling to provide real-time insights into network performance. Redesigns the dashboard with advanced metrics and charts for enhanced visualization of system data.
**Changes:** 9 files, 1245 lines
**Languages:** C (1245 lines)
## Version 0.5.0 - 2025-12-28
Enhances authentication security by preventing timing attacks and clearing sensitive memory, while adding rate limiting to protect against abusive client requests. Enables SSL hostname verification and preferred cipher suites for improved connection security, and fixes request denial when rate limit allocation fails.

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);
}
}

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);
}
}

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;
}

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

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);

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;
}

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

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

277
tests/test_logging.c Executable file
View File

@ -0,0 +1,277 @@
#include "test_framework.h"
#include "../src/logging.h"
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
void test_logging_debug_mode(void) {
TEST_SUITE_BEGIN("Logging Debug Mode");
logging_set_debug(0);
TEST_ASSERT_EQ(0, logging_get_debug(), "Debug mode disabled");
logging_set_debug(1);
TEST_ASSERT_EQ(1, logging_get_debug(), "Debug mode enabled");
logging_set_debug(0);
TEST_ASSERT_EQ(0, logging_get_debug(), "Debug mode disabled again");
TEST_SUITE_END();
}
void test_logging_set_file(void) {
TEST_SUITE_BEGIN("Logging Set File");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
TEST_ASSERT(fd >= 0, "Temp file created");
close(fd);
int result = logging_set_file(tmp_path);
TEST_ASSERT_EQ(0, result, "Set log file succeeds");
log_info("Test log message");
struct stat st;
stat(tmp_path, &st);
TEST_ASSERT(st.st_size > 0, "Log file has content");
logging_cleanup();
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_set_file_null(void) {
TEST_SUITE_BEGIN("Logging Set File NULL");
int result = logging_set_file(NULL);
TEST_ASSERT_EQ(0, result, "NULL path returns to stdout");
log_info("Test message to stdout");
TEST_ASSERT(1, "Logging to stdout works");
TEST_SUITE_END();
}
void test_logging_set_file_invalid(void) {
TEST_SUITE_BEGIN("Logging Set File Invalid Path");
int result = logging_set_file("/nonexistent/directory/log.txt");
TEST_ASSERT_EQ(-1, result, "Invalid path returns -1");
TEST_SUITE_END();
}
void test_logging_log_functions(void) {
TEST_SUITE_BEGIN("Logging Log Functions");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
log_info("Info message: %d", 42);
log_error("Error message: %s", "test error");
logging_set_debug(1);
log_debug("Debug message: %s", "debug info");
logging_set_debug(0);
log_debug("This should not appear");
struct stat st;
stat(tmp_path, &st);
TEST_ASSERT(st.st_size > 0, "Log messages written");
FILE *f = fopen(tmp_path, "r");
char content[4096] = {0};
if (f) {
size_t bytes_read = fread(content, 1, sizeof(content) - 1, f);
(void)bytes_read;
fclose(f);
}
TEST_ASSERT(strstr(content, "INFO") != NULL, "INFO level present");
TEST_ASSERT(strstr(content, "ERROR") != NULL, "ERROR level present");
TEST_ASSERT(strstr(content, "DEBUG") != NULL, "DEBUG level present");
TEST_ASSERT(strstr(content, "42") != NULL, "Info param present");
TEST_ASSERT(strstr(content, "test error") != NULL, "Error param present");
logging_cleanup();
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_cleanup(void) {
TEST_SUITE_BEGIN("Logging Cleanup");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
log_info("Before cleanup");
logging_cleanup();
log_info("After cleanup to stdout");
TEST_ASSERT(1, "Cleanup completed and logging works");
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_multiple_files(void) {
TEST_SUITE_BEGIN("Logging Multiple File Switches");
char tmp1[] = "/tmp/test_rproxy_log1_XXXXXX";
char tmp2[] = "/tmp/test_rproxy_log2_XXXXXX";
int fd1 = mkstemp(tmp1);
int fd2 = mkstemp(tmp2);
close(fd1);
close(fd2);
logging_set_file(tmp1);
log_info("Message to file 1");
logging_set_file(tmp2);
log_info("Message to file 2");
struct stat st1, st2;
stat(tmp1, &st1);
stat(tmp2, &st2);
TEST_ASSERT(st1.st_size > 0, "First file has content");
TEST_ASSERT(st2.st_size > 0, "Second file has content");
logging_cleanup();
unlink(tmp1);
unlink(tmp2);
TEST_SUITE_END();
}
void test_logging_error_with_errno(void) {
TEST_SUITE_BEGIN("Logging Error With Errno");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
errno = ENOENT;
log_error("File not found error");
errno = 0;
log_error("Error without errno");
errno = EPERM;
log_error("Permission denied: %s", "/test/file");
errno = 0;
logging_cleanup();
FILE *f = fopen(tmp_path, "r");
char content[4096] = {0};
if (f) {
size_t bytes_read = fread(content, 1, sizeof(content) - 1, f);
(void)bytes_read;
fclose(f);
}
TEST_ASSERT(strstr(content, "File not found") != NULL, "First error logged");
TEST_ASSERT(strstr(content, "without errno") != NULL, "Second error logged");
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_debug_disabled(void) {
TEST_SUITE_BEGIN("Logging Debug Disabled");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
logging_set_debug(0);
log_debug("This should not appear");
log_info("This should appear");
struct stat st;
stat(tmp_path, &st);
TEST_ASSERT(st.st_size > 0, "File has some content");
FILE *f = fopen(tmp_path, "r");
char content[4096] = {0};
if (f) {
size_t bytes_read = fread(content, 1, sizeof(content) - 1, f);
(void)bytes_read;
fclose(f);
}
TEST_ASSERT(strstr(content, "should appear") != NULL, "Info message present");
logging_cleanup();
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_format_strings(void) {
TEST_SUITE_BEGIN("Logging Format Strings");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
log_info("Int: %d, String: %s, Float: %.2f", 42, "test", 3.14);
log_error("Code: %d, Msg: %s", 500, "Internal error");
logging_set_debug(1);
log_debug("Debug: %s %d", "value", 123);
logging_set_debug(0);
logging_cleanup();
FILE *f = fopen(tmp_path, "r");
char content[4096] = {0};
if (f) {
size_t bytes_read = fread(content, 1, sizeof(content) - 1, f);
(void)bytes_read;
fclose(f);
}
TEST_ASSERT(strstr(content, "42") != NULL, "Int formatted");
TEST_ASSERT(strstr(content, "test") != NULL, "String formatted");
TEST_ASSERT(strstr(content, "500") != NULL, "Error code formatted");
unlink(tmp_path);
TEST_SUITE_END();
}
void run_logging_tests(void) {
test_logging_debug_mode();
test_logging_set_file();
test_logging_set_file_null();
test_logging_set_file_invalid();
test_logging_log_functions();
test_logging_cleanup();
test_logging_multiple_files();
test_logging_error_with_errno();
test_logging_debug_disabled();
test_logging_format_strings();
}