Files
RustProxy/src/admin/web/app/overview-wide-metrics.js
T
2026-10-15 09:37:47 +02:00

120 lines
6.4 KiB
JavaScript

window.rustProxyWideOverview = (() => {
const limit = () => window.rustProxyOverviewSamples?.getLimit?.() ?? 60;
const configurations = {
wide_protocol_rates: {
unit: "rate", names: ["h1", "h2", "h3"], counters: s =>
[s.metrics?.http1_requests, s.metrics?.http2_requests, s.metrics?.http3_requests]
},
wide_cpu: {
unit: "percent", names: ["system", "process"], values: s =>
[(s.live_system || s.system)?.cpu_percent, (s.live_system || s.system)?.process_cpu_percent]
},
wide_memory: {
unit: "bytes", names: ["process", "cache"], values: s =>
[(s.live_system || s.system)?.process_rss_bytes, s.cache?.resident_bytes]
},
wide_cache_activity: {
unit: "rate", names: ["hits", "misses"], counters: s =>
[s.cache?.hits, s.cache?.misses]
},
wide_connections: {
unit: "count", names: ["open", "websockets"], values: s =>
[s.metrics?.active_connections, s.metrics?.websocket_active]
},
wide_backend_health: {
unit: "count", names: ["up", "down"], values: s => {
if (!Array.isArray(s.backend_pools)) return [null, null];
const members = s.backend_pools.flatMap(pool => pool.members || []);
return [members.filter(m => m.status === "up").length, members.filter(m => m.status === "down").length];
}
},
wide_latency: {
unit: "ms", names: ["response", "probe"], values: s => {
if (!Array.isArray(s.backend_pools)) return [s.metrics?.mean_duration_ms, null];
const members = s.backend_pools.flatMap(pool => pool.members || []);
const probes = members.filter(m => m.status === "up" && m.health_mode !== "off"
&& finite(m.last_probe_ms) !== null && finite(m.latency_ms) !== null
&& Number(m.last_probe_ms) > 0);
return [s.metrics?.mean_duration_ms,
probes.length ? probes.reduce((total, m) => total + Number(m.latency_ms), 0) / probes.length : null];
}
},
wide_transform_activity: {
unit: "rate", names: ["applied", "cache_hits", "skipped", "errors"], total: true,
counters: s => [s.transform_metrics?.applied, s.transform_metrics?.cache_hits,
s.transform_metrics?.skipped, s.transform_metrics?.errors]
},
wide_compression_activity: {
unit: "rate", names: ["gzip", "br", "zstd", "deflate"], total: true,
counters: s => [s.metrics?.compression_gzip, s.metrics?.compression_br,
s.metrics?.compression_zstd, s.metrics?.compression_deflate]
},
};
const history = new Map(), baselines = new Map(), last = new Map();
let revision = null;
const finite = value => value != null && Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value) : null;
const format = (n, unit) => n === null ? "-" : unit === "bytes" ? fmtBytes(n)
: unit === "percent" ? `${fmt(n, 1)}%` : unit === "ms" ? `${fmt(n, 1)} ms`
: unit === "rate" ? `${fmt(n, 1)}/s` : fmt(n);
const tile = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
function counterRates(id, values, now, record) {
const counters = values.map(finite);
if (counters.some(n => n === null)) { baselines.delete(id); return counters.map(() => null); }
if (!record) return last.get(id) || counters.map(() => null);
const result = rustProxyRateSampling.difference(baselines.get(id), counters, now, rustProxyRateSampling.cadence("overview_seconds"));
baselines.set(id, result.next);
return result.deltas ? result.deltas.map(delta => delta / result.seconds) : counters.map(() => null);
}
function path(values, peak) {
const count = values.length;
let result = "", drawing = false;
for (let i = 0; i < count; i++) {
const value = values[i];
if (value === null) { drawing = false; continue; }
const x = Math.round(600 * i / Math.max(1, count - 1));
const y = Math.round(137 - 120 * value / peak);
result += `${drawing ? " L" : " M"}${x} ${y}`;
drawing = true;
}
return result.trim();
}
function render(status, record = true) {
if (!status || currentPanel !== "overview") return;
if (revision !== status.revision) { reset(); revision = status.revision; }
const now = performance.now();
for (const [id, config] of Object.entries(configurations)) {
const node = tile(id);
if (!node || node.hidden) { baselines.delete(id); continue; }
const values = config.counters ? counterRates(id, config.counters(status), now, record)
: config.values(status).map(finite);
const series = history.get(id) || config.names.map(() => []);
if (record) {
values.forEach((value, i) => {
series[i].push(value);
if (series[i].length > limit()) series[i].shift();
});
history.set(id, series);
last.set(id, values);
}
const peak = Math.max(config.unit === "percent" ? 100 : 1,
...series.flat().filter(n => n !== null));
config.names.forEach((key, i) => {
const value = values[i];
const field = $(`overview-wide-${id}-${key}-value`);
if (field) field.textContent = format(value, config.unit);
$(`overview-wide-${id}-${key}-line`)?.setAttribute("d", path(series[i], peak));
});
const summary = $(`overview-wide-${id}-summary`);
if (summary) summary.textContent = config.total
? format(values.every(value => value !== null) ? values.reduce((sum, value) => sum + value, 0) : null, config.unit)
: format(values[0], config.unit);
}
}
function reset() { history.clear(); baselines.clear(); last.clear(); revision = null; }
window.rustProxyOverviewSamples?.onChange?.(() => {
for (const series of history.values()) for (const values of series) window.rustProxyOverviewSamples.trim(values);
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") render(latestStatus, false);
});
return { render, reset };
})();