96 lines
5.2 KiB
JavaScript
96 lines
5.2 KiB
JavaScript
// Existing status snapshots only: no API calls, bounded histories and honest no-data.
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import vm from 'node:vm';
|
|
|
|
const source = fs.readFileSync('src/admin/web/app/overview-extra-metrics.js', 'utf8');
|
|
const registry = JSON.parse(fs.readFileSync('src/admin/web/dashboard-widgets.json', 'utf8'));
|
|
const ids = registry.filter(widget => widget.category === 'performance').map(widget => widget.id);
|
|
assert.equal(ids.length, 18);
|
|
const widgets = new Map(ids.map(id => [id, { hidden: false }]));
|
|
const fields = new Map();
|
|
const field = id => {
|
|
if (!fields.has(id)) fields.set(id, { textContent: '-', points: '', setAttribute(name, value) { this[name] = value; } });
|
|
return fields.get(id);
|
|
};
|
|
let now = 1000;
|
|
const context = {
|
|
window: {}, currentPanel: 'overview', performance: { now: () => now },
|
|
t: key => key, fmt: (v, places = 0) => Number(v).toFixed(places),
|
|
fmtBytes: v => `${v} B`, $: field,
|
|
document: { querySelector: selector => widgets.get(selector.match(/data-dashboard-widget="([^"]+)"/)?.[1]) || null },
|
|
};
|
|
vm.createContext(context);
|
|
vm.runInContext(source, context);
|
|
const render = context.window.rustProxyExtraOverview.render;
|
|
const text = id => field(`overview-extra-${id}`).textContent;
|
|
const points = id => field(`overview-extra-${id}-line`).points;
|
|
const start = {
|
|
metrics: { requests: 100, http1_requests: 70, http2_requests: 30, http3_requests: 0, tls_handshake_errors: 3, errors: 10, active_connections: 5, active: 2, websocket_active: 1, mean_duration_ms: 25 },
|
|
system: { process_cpu_percent: 12, process_rss_bytes: 1000, open_fds: 10, max_fds: 500 },
|
|
cache: { hits: 10, misses: 10, resident_bytes: 10, hot_limit_bytes: 100 },
|
|
backend_pools: [{ members: [{status: 'up'}, {status: 'down'}] }],
|
|
};
|
|
render(start);
|
|
assert.equal(text('extra_process_cpu'), '12.0%');
|
|
assert.equal(text('extra_process_rss'), '1000 B');
|
|
assert.equal(text('extra_backend_unhealthy'), '1');
|
|
for (const id of ['extra_tls_error_rate','extra_request_error_rate','extra_cache_hit_rate','extra_h2_share'])
|
|
assert.equal(text(id), '-', `initial ${id} delta must not be invented`);
|
|
now = 3000;
|
|
const next = structuredClone(start);
|
|
next.metrics = { ...start.metrics, requests: 130, http1_requests: 84, http2_requests: 45, http3_requests: 1, tls_handshake_errors: 5, errors: 16, active_connections: 6, active: 4, websocket_active: 3, mean_duration_ms: 40 };
|
|
next.system = { ...start.system, process_cpu_percent: 22, process_rss_bytes: 2000, open_fds: 20 };
|
|
next.cache = { ...start.cache, hits: 20, misses: 15, resident_bytes: 25 };
|
|
next.backend_pools[0].members[0].status = 'down';
|
|
render(next);
|
|
assert.equal(text('extra_tls_error_rate'), '1.0/s');
|
|
assert.equal(text('extra_request_error_rate'), '3.0/s');
|
|
assert.equal(text('extra_cache_hit_rate'), '66.7%');
|
|
assert.equal(text('extra_h2_share'), '50.0%');
|
|
assert.equal(text('extra_cache_memory'), '25.0%');
|
|
assert.equal(text('extra_response_latency'), '40.0 ms');
|
|
assert.equal(text('extra_backend_unhealthy'), '2');
|
|
assert.equal(text('extra_total_rps'), '15.0/s');
|
|
assert.equal(text('extra_active_exchanges'), '4');
|
|
assert.equal(text('extra_h1_share'), '46.7%');
|
|
assert.equal(text('extra_h3_share'), '3.3%');
|
|
assert.equal(text('extra_cache_miss_rate'), '2.5/s');
|
|
assert.equal(text('extra_backend_active'), '0');
|
|
assert.equal(points('extra_process_cpu').trim().split(/\s+/).length, 2);
|
|
// Hiding a chart stops sampling; stale cumulative counters are never used as an instant rate.
|
|
widgets.get('extra_tls_error_rate').hidden = true;
|
|
now = 5000;
|
|
render({ ...next, metrics: { ...next.metrics, tls_handshake_errors: 50 } });
|
|
assert.equal(text('extra_tls_error_rate'), '1.0/s');
|
|
widgets.get('extra_tls_error_rate').hidden = false;
|
|
now = 6000;
|
|
render({ ...next, metrics: { ...next.metrics, tls_handshake_errors: 60 } });
|
|
assert.equal(text('extra_tls_error_rate'), '-', 'first fresh visible sample establishes a new baseline');
|
|
// After a reset, don't return a negative rate or denominator percentage.
|
|
now = 7000;
|
|
render({ ...next, metrics: { ...next.metrics, requests: 1, http2_requests: 0, tls_handshake_errors: 0, errors: 0 }, cache: { ...next.cache, hits: 0, misses: 0 } });
|
|
assert.equal(text('extra_h2_share'), '-');
|
|
assert.equal(text('extra_request_error_rate'), '-');
|
|
assert.equal(text('extra_tls_error_rate'), '-');
|
|
// Explicit unavailability is not represented as a measured zero.
|
|
now = 8000;
|
|
render({ metrics: {}, system: {}, cache: {} });
|
|
assert.equal(text('extra_process_cpu'), '-');
|
|
assert.equal(text('extra_websocket_active'), '-');
|
|
assert.equal(text('extra_backend_unhealthy'), '-');
|
|
context.window.rustProxyExtraOverview.reset();
|
|
now = 9000;
|
|
for (let index = 0; index < 50; index++) {
|
|
now += 1000;
|
|
render({ ...next, system: { ...next.system, process_cpu_percent: index } });
|
|
}
|
|
assert.equal(points('extra_process_cpu').trim().split(/\s+/).length, 30, 'bounded history');
|
|
context.currentPanel = 'vhosts';
|
|
const pointBefore = points('extra_process_cpu');
|
|
now += 1000;
|
|
render(next);
|
|
assert.equal(points('extra_process_cpu'), pointBefore, 'no extra sampling outside Overview');
|
|
assert.match(source, /if \(!tile \|\| tile.hidden\)/);
|
|
console.log('PASS: 18 extra metric series, time-based deltas, no-data, counter resets, hidden opt-ins and 30-sample bounds');
|