230 lines
13 KiB
JavaScript
230 lines
13 KiB
JavaScript
window.rustProxyWebOverview = (() => {
|
|
const valid = n => n != null && Number.isFinite(Number(n)) && Number(n) >= 0 ? Number(n) : null;
|
|
const show = (id, text) => { const el = $(id); if (el) el.textContent = text; };
|
|
const fmtRate = n => n == null ? "-" : `${fmt(n, 1)}/s`;
|
|
const tile = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
|
|
const seriesNames = {
|
|
www_requests: ["all", "php"], www_latency: ["web", "php"],
|
|
www_failures: ["timeout", "connection", "protocol", "capacity"],
|
|
www_traffic: ["nonphp", "php"],
|
|
www_status_trend: ["2xx", "3xx", "4xx", "5xx"]
|
|
};
|
|
const histories = new Map(), serviceBaselines = new Map();
|
|
let baseline = null, revision = null, last = null;
|
|
function aggregate(s) {
|
|
const services = s?.web_server_metrics;
|
|
if (!services || typeof services !== "object") return null;
|
|
const web = {
|
|
requests: 0, response_headers_us: 0, response_bytes: 0,
|
|
status: [0, 0, 0, 0], kinds: [0, 0, 0], methods: [0, 0, 0, 0, 0],
|
|
selected: Array(8).fill(0), active: 0
|
|
};
|
|
const php = {
|
|
requests: 0, response_headers_us: 0, response_bytes: 0,
|
|
failures: Array(9).fill(0), capacity_limit: 0, capacity_used: 0, count: 0
|
|
};
|
|
const perService = {};
|
|
for (const [id, entry] of Object.entries(services)) {
|
|
const w = entry?.web;
|
|
if (!w || valid(w.requests) === null) continue;
|
|
for (const key of ["requests", "response_headers_us", "response_bytes"])
|
|
web[key] += valid(w[key]) || 0;
|
|
for (const [i, key] of ["status_2xx", "status_3xx", "status_4xx", "status_5xx"].entries())
|
|
web.status[i] += valid(w[key]) || 0;
|
|
for (const [i, key] of ["static_requests", "php_requests", "other_requests"].entries())
|
|
web.kinds[i] += valid(w[key]) || 0;
|
|
["methods", "selected_status"].forEach((field, index) => {
|
|
const output = index ? web.selected : web.methods;
|
|
output.forEach((_, i) => { output[i] += valid(w[field]?.[i]) || 0; });
|
|
});
|
|
web.active += valid(w.active_headers) || 0;
|
|
perService[id] = valid(w.requests);
|
|
if (entry.php?.metrics) {
|
|
php.count++;
|
|
const pm = entry.php.metrics;
|
|
for (const key of ["requests", "response_headers_us", "response_bytes"])
|
|
php[key] += valid(pm[key]) || 0;
|
|
php.failures.forEach((_, i) => { php.failures[i] += valid(pm.failures?.[i]) || 0; });
|
|
php.capacity_limit += valid(entry.php.capacity_limit) || 0;
|
|
php.capacity_used += valid(entry.php.capacity_used) || 0;
|
|
}
|
|
}
|
|
return Object.keys(perService).length ? { web, php, perService } : null;
|
|
}
|
|
function compact(a) {
|
|
return [a.web.requests, ...a.web.status, ...a.web.kinds, ...a.web.methods,
|
|
a.web.response_headers_us, a.web.response_bytes, a.php.requests,
|
|
a.php.response_headers_us, a.php.response_bytes, ...a.php.failures];
|
|
}
|
|
function rates(a, record) {
|
|
if (!record) return last;
|
|
const sample = rustProxyRateSampling.difference(baseline, compact(a), performance.now(),
|
|
rustProxyRateSampling.cadence("overview_seconds"));
|
|
baseline = sample.next;
|
|
if (!sample.deltas) { last = null; return null; }
|
|
const d = sample.deltas, seconds = sample.seconds;
|
|
const perSec = value => value / seconds;
|
|
last = {
|
|
rps: [perSec(d[0]), perSec(d[15])],
|
|
status: d.slice(1, 5).map(perSec),
|
|
latency: [d[0] > 0 ? d[13] / d[0] / 1000 : null,
|
|
d[15] > 0 ? d[16] / d[15] / 1000 : null],
|
|
traffic: [perSec(Math.max(0, d[14] - d[17])), perSec(d[17])],
|
|
failures: [perSec(d[19] + d[21] + d[23]),
|
|
perSec(d[20] + d[22] + d[24]), perSec(d[25] + d[26]), perSec(d[18])],
|
|
errorRate: d[0] > 0 ? d[4] / d[0] * 100 : null,
|
|
phpRps: perSec(d[15]),
|
|
};
|
|
return last;
|
|
}
|
|
function chartPath(values, peak) {
|
|
let output = "", painting = false;
|
|
values.forEach((value, i) => {
|
|
if (value == null) { painting = false; return; }
|
|
output += `${painting ? " L" : "M"}${Math.round(i * 600 / Math.max(1, values.length - 1))} ${Math.round(137 - 120 * value / peak)}`;
|
|
painting = true;
|
|
});
|
|
return output;
|
|
}
|
|
function smallPath(values) {
|
|
const peak = window.rustProxyChartScale.peak("web:php-small", "rate", [values]);
|
|
return values.filter(value => value !== null).map((value, i, present) => `${i * 300 / Math.max(1, present.length - 1)},${55 - value / peak * 50}`).join(" ");
|
|
}
|
|
function ring(id, amounts) {
|
|
const total = amounts.reduce((n, value) => n + value, 0);
|
|
let offset = 0;
|
|
amounts.forEach((amount, i) => {
|
|
const share = total ? amount / total * 100 : 0;
|
|
const arc = $(`${id}-arc-${i}`);
|
|
arc?.setAttribute("stroke-dasharray", `${share} ${100 - share}`);
|
|
arc?.setAttribute("stroke-dashoffset", String(-offset));
|
|
offset += share;
|
|
show(`${id}-count-${i}`, fmt(amount));
|
|
});
|
|
show(`${id}-value`, total ? `${fmt(amounts[0] / total * 100, 1)}%` : "-");
|
|
$(`${id}-chart`)?.setAttribute("aria-label", `${t(id === "www_status" ? "webMetricStatus" : "webMetricKind")}: ${amounts.join(" / ")}`);
|
|
}
|
|
function meter(id, percent) {
|
|
window.rustProxyExtraOverview?.meter?.(id, percent);
|
|
}
|
|
function clearUnavailable() {
|
|
for (const id of ["www_requests", "www_latency", "www_failures", "www_traffic",
|
|
"www_status_trend", "www_php_requests", "www_capacity", "www_error_rate",
|
|
"www_methods", "www_top_services"]) show(`${id}-value`, "-");
|
|
for (const [id, names] of Object.entries(seriesNames)) {
|
|
for (const name of names) {
|
|
show(`${id}-${name}-value`, "-");
|
|
$(`${id}-${name}-line`)?.setAttribute("d", "");
|
|
}
|
|
}
|
|
$("www_php_requests-line")?.setAttribute("points", "");
|
|
$("www_top_services-bars")?.replaceChildren();
|
|
show("www_methods-detail", "-");
|
|
for (let i = 0; i < 5; i++) {
|
|
const segment = $(`www_methods-segment-${i}`);
|
|
if (segment) { segment.style.width = "0%"; segment.title = ""; }
|
|
}
|
|
show("www_capacity-detail", t("webLiveNoPhp"));
|
|
show("www_error_rate-detail", t("webMetricErrorRateHelp"));
|
|
meter("www_capacity", null);
|
|
meter("www_error_rate", null);
|
|
ring("www_status", [0, 0, 0, 0]);
|
|
ring("www_kind", [0, 0, 0]);
|
|
}
|
|
function render(s, record = true) {
|
|
const a = aggregate(s);
|
|
if (revision !== s?.revision) { reset(); revision = s?.revision; }
|
|
if (!a) { reset(); revision = s?.revision; clearUnavailable(); return; }
|
|
if (currentPanel !== "overview") { baseline = null; return; }
|
|
const r = rates(a, record);
|
|
ring("www_status", a.web.status);
|
|
ring("www_kind", a.web.kinds);
|
|
const totalMethods = a.web.methods.reduce((n, v) => n + v, 0);
|
|
show("www_methods-value", totalMethods ? fmt(totalMethods) : "-");
|
|
a.web.methods.forEach((value, i) => {
|
|
const segment = $(`www_methods-segment-${i}`);
|
|
if (segment) { segment.style.width = `${totalMethods ? value / totalMethods * 100 : 0}%`; segment.title = `${["GET", "POST", "PUT", "DELETE", "Other"][i]}: ${fmt(value)}`; }
|
|
});
|
|
show("www_methods-detail", ["GET", "POST", "PUT", "DELETE", "Other"].map((name, i) => `${name} ${fmt(a.web.methods[i])}`).join(" · "));
|
|
const { capacity_limit: max, capacity_used: used } = a.php;
|
|
show("www_capacity-value", max ? `${fmt(used)} / ${fmt(max)}` : "-");
|
|
meter("www_capacity", max ? used / max * 100 : null);
|
|
show("www_capacity-detail", max ? `${fmt(used / max * 100, 1)}%` : t("webLiveNoPhp"));
|
|
show("www_error_rate-value", r?.errorRate == null ? "-" : `${fmt(r.errorRate, 1)}%`);
|
|
meter("www_error_rate", r?.errorRate);
|
|
show("www_error_rate-detail", t("webMetricErrorRateHelp"));
|
|
show("www_php_requests-value", fmtRate(r?.phpRps));
|
|
if (record && r) {
|
|
const values = histories.get("www_php_requests") || [];
|
|
values.push(r.phpRps); window.rustProxyOverviewSamples.trim(values);
|
|
histories.set("www_php_requests", values);
|
|
}
|
|
$(`www_php_requests-line`)?.setAttribute("points", smallPath(histories.get("www_php_requests") || []));
|
|
const set = {
|
|
www_requests: r?.rps, www_latency: r?.latency, www_failures: r?.failures,
|
|
www_traffic: r?.traffic, www_status_trend: r?.status,
|
|
};
|
|
for (const [id, names] of Object.entries(seriesNames)) {
|
|
const series = histories.get(id) || names.map(() => []);
|
|
// A resumed/overdue interval has no rate; skipping that entire
|
|
// snapshot keeps existing paths connected without inventing 0/s.
|
|
// A genuinely unavailable series inside a valid interval stays null.
|
|
if (record && r) {
|
|
names.forEach((_, i) => { series[i].push(set[id]?.[i] ?? null); window.rustProxyOverviewSamples.trim(series[i]); });
|
|
histories.set(id, series);
|
|
}
|
|
const unit = id === "www_latency" ? "ms" : id === "www_traffic" ? "bytes/s" : "rate";
|
|
const peak = window.rustProxyChartScale.peak(`web:${id}`, unit, series);
|
|
names.forEach((name, i) => {
|
|
const value = set[id]?.[i];
|
|
const label = id === "www_latency" ? (value == null ? "-" : `${fmt(value, 1)} ms`)
|
|
: id === "www_traffic" ? (value == null ? "-" : `${fmtBytes(value)}/s`) : fmtRate(value);
|
|
show(`${id}-${name}-value`, label);
|
|
$(`${id}-${name}-line`)?.setAttribute("d", chartPath(series[i], peak));
|
|
});
|
|
const summary = id === "www_latency" ? (set[id]?.[0] == null ? "-" : `${fmt(set[id][0], 1)} ms`)
|
|
: id === "www_traffic" ? (set[id] ? `${fmtBytes(set[id][0] + set[id][1])}/s` : "-")
|
|
: fmtRate(set[id]?.[0]);
|
|
show(`${id}-value`, summary);
|
|
}
|
|
if (record) {
|
|
const currentIds = new Set(Object.keys(a.perService));
|
|
for (const id of serviceBaselines.keys()) if (!currentIds.has(id)) serviceBaselines.delete(id);
|
|
const rows = Object.entries(a.perService).map(([id, total]) => {
|
|
const old = serviceBaselines.get(id);
|
|
const delta = rustProxyRateSampling.difference(old, [total], performance.now(),
|
|
rustProxyRateSampling.cadence("overview_seconds"));
|
|
serviceBaselines.set(id, delta.next);
|
|
return { id, value: delta.deltas ? delta.deltas[0] / delta.seconds : null };
|
|
});
|
|
if (rows.some(item => item.value !== null)) {
|
|
rows.sort((x, y) => (y.value ?? -1) - (x.value ?? -1));
|
|
const top = rows.filter(item => item.value !== null).slice(0, 6);
|
|
const container = $("www_top_services-bars");
|
|
if (container && !tile("www_top_services")?.hidden) {
|
|
const peak = Math.max(1, ...top.map(item => item.value));
|
|
container.replaceChildren(...top.map(item => {
|
|
const row = document.createElement("div"); row.className = "www-top-row";
|
|
const name = document.createElement("span"); name.textContent = item.id;
|
|
const meter = document.createElement("div"); meter.className = "www-top-meter";
|
|
const fill = document.createElement("span"); fill.style.width = `${item.value / peak * 100}%`;
|
|
meter.append(fill);
|
|
const value = document.createElement("b"); value.textContent = fmtRate(item.value);
|
|
row.append(name, meter, value); return row;
|
|
}));
|
|
}
|
|
show("www_top_services-value", fmtRate(top.reduce((n, item) => n + item.value, 0)));
|
|
}
|
|
}
|
|
}
|
|
function reset() { histories.clear(); serviceBaselines.clear(); baseline = null; last = null; revision = null; window.rustProxyChartScale.reset("web:"); }
|
|
window.rustProxyOverviewSamples.onChange(() => {
|
|
for (const history of histories.values()) {
|
|
if (Array.isArray(history[0])) history.forEach(window.rustProxyOverviewSamples.trim);
|
|
else window.rustProxyOverviewSamples.trim(history);
|
|
}
|
|
if (latestStatus && currentPanel === "overview") render(latestStatus, false);
|
|
});
|
|
return { render, reset, aggregate };
|
|
})();
|