120 lines
6.4 KiB
JavaScript
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 };
|
|
})();
|