Files
RustProxy/src/admin/web/app/overview-extra-metrics.js
T
2026-11-07 13:56:11 +01:00

236 lines
14 KiB
JavaScript

window.rustProxyExtraOverview = (() => {
const limit = () => window.rustProxyOverviewSamples?.getLimit?.() ?? 60;
const names = [
"extra_process_cpu", "extra_process_rss", "extra_process_fds", "extra_open_connections",
"extra_websocket_active", "extra_tls_error_rate", "extra_request_error_rate",
"extra_cache_hit_rate", "extra_cache_memory", "extra_cold_queue_memory", "extra_cold_storage", "extra_h2_share",
"extra_response_latency", "extra_backend_unhealthy",
"extra_total_rps", "extra_active_exchanges", "extra_h1_share", "extra_h3_share",
"extra_cache_miss_rate", "extra_backend_active",
"extra_backends_healthy", "extra_error_share", "extra_rejected_rate", "extra_cert_expiry",
];
const bounded = new Set([
"extra_process_fds", "extra_cache_memory", "extra_cold_queue_memory", "extra_cold_storage",
"extra_h1_share", "extra_h2_share", "extra_h3_share",
"extra_backends_healthy", "extra_cert_expiry",
]);
const histories = new Map(), lastCounter = new Map(), lastValue = new Map();
const lastUnits = new Map(), lastSegments = new Map();
const positive = value => value != null && Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value) : null;
const node = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
function meter(id, percent) {
const track = $(`${id}-meter`), fill = $(`${id}-meter-fill`);
if (!track || !fill) return;
const valid = positive(percent), boundedPercent = valid === null ? 0 : Math.min(100, valid);
fill.style.width = `${boundedPercent}%`;
if (valid === null) {
track.removeAttribute("aria-valuenow");
track.setAttribute("aria-valuetext", "-");
} else {
track.setAttribute("aria-valuenow", String(boundedPercent));
track.setAttribute("aria-valuetext", `${fmt(valid, 1)}%`);
}
}
function donut(prefix, hits, misses) {
const hit = positive(hits), miss = positive(misses);
const total = hit !== null && miss !== null ? hit + miss : 0;
const share = total > 0 ? hit / total * 100 : 0;
const hitArc = $(`${prefix}-hit-arc`), missArc = $(`${prefix}-miss-arc`);
if (hitArc) hitArc.setAttribute("stroke-dasharray", `${share} ${100 - share}`);
if (missArc) {
missArc.setAttribute("stroke-dasharray", `${100 - share} ${share}`);
missArc.setAttribute("stroke-dashoffset", String(-share));
}
const hitCount = $(`${prefix}-hit-count`), missCount = $(`${prefix}-miss-count`);
if (hitCount) hitCount.textContent = hit === null ? "-" : fmt(hit);
if (missCount) missCount.textContent = miss === null ? "-" : fmt(miss);
const graphic = $(`${prefix}-chart`);
if (graphic) graphic.setAttribute("aria-label", t("overviewCacheHitsMisses")
.replace("{hits}", hit === null ? "-" : fmt(hit))
.replace("{misses}", miss === null ? "-" : fmt(miss)));
}
function elapsedDelta(id, counters, now) {
const result = rustProxyRateSampling.difference(lastCounter.get(id), counters, now,
rustProxyRateSampling.cadence("overview_seconds"));
lastCounter.set(id, result.next);
return result.deltas ? result : null;
}
function ramBudget(bytes, budget) {
const used = positive(bytes), maximum = positive(budget);
const percent = maximum > 0 && used !== null ? used / maximum * 100 : null;
return {
value: percent === null ? used : percent,
unit: percent === null ? "bytes" : "percent", meterPercent: percent,
detail: used === null ? "" : `${fmtBytes(used)} / ${maximum > 0 ? fmtBytes(maximum) : t("overviewMeterNoLimit")}`,
};
}
function read(id, status, now, record) {
const m = status.metrics || {}, d = status.system || {}, live = status.live_system || d, c = status.cache || {};
const direct = {
extra_process_cpu: live.process_cpu_percent,
extra_process_rss: live.process_rss_bytes,
extra_process_fds: d.open_fds,
extra_open_connections: m.active_connections,
extra_websocket_active: m.websocket_active,
extra_response_latency: m.mean_duration_ms,
extra_active_exchanges: m.active,
};
if (Object.hasOwn(direct, id)) {
const numeric = positive(direct[id]), fdLimit = positive(d.max_fds);
return {
value: numeric, unit: id === "extra_process_cpu" ? "percent" : id === "extra_process_rss" ? "bytes"
: id === "extra_response_latency" ? "ms" : "count",
meterPercent: id === "extra_process_fds" && fdLimit > 0 && numeric !== null ? numeric / fdLimit * 100 : null,
detail: id === "extra_process_fds"
? (fdLimit > 0 ? `${t("overviewExtraFdLimit")}: ${fmt(fdLimit)}` : t("overviewMeterNoLimit")) : "",
};
}
if (id === "extra_backend_active") {
const members = Array.isArray(status.backend_pools) ? status.backend_pools.flatMap(pool => pool.members || []) : null;
return {
value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
unit: "count", detail: ""
};
}
if (id === "extra_backend_unhealthy") {
return {
value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null,
unit: "count", detail: ""
};
}
if (id === "extra_backends_healthy") {
const members = Array.isArray(status.backend_pools)
? status.backend_pools.flatMap(pool => (pool.members || []).map(member => ({ pool: pool.id, ...member }))) : null;
if (!members) return { value: null, unit: "count", detail: "" };
const monitored = members.filter(member => member.health_check_enabled !== false && member.health_mode !== "off");
const down = monitored.filter(member => member.status === "down" || member.healthy === false);
const up = monitored.length - down.length;
return {
value: monitored.length ? `${fmt(up)} / ${fmt(monitored.length)}` : null, unit: "text",
meterPercent: monitored.length ? up / monitored.length * 100 : null,
detail: !monitored.length ? t("overviewExtraBackendsNone")
: down.length ? `${t("overviewExtraBackendsDown")}: ${down.slice(0, 3).map(member => `${member.pool}/${member.id}`).join(", ")}${down.length > 3 ? " …" : ""}`
: t("overviewExtraBackendsAllUp"),
};
}
if (id === "extra_cert_expiry") {
const inventory = typeof certificateInventory !== "undefined" && certificateInventory instanceof Map ? [...certificateInventory.values()] : [];
const dated = inventory.filter(cert => Number.isFinite(Number(cert?.days_remaining)))
.sort((a, b) => Number(a.days_remaining) - Number(b.days_remaining));
if (!dated.length) return { value: null, unit: "days", detail: t("overviewExtraCertNone") };
const nearest = dated[0], days = Number(nearest.days_remaining);
const name = (Array.isArray(nearest.covered_hosts) && nearest.covered_hosts[0]) || (Array.isArray(nearest.hosts) && nearest.hosts[0]) || nearest.id;
return { value: days, unit: "days", meterPercent: Math.max(0, Math.min(100, days / 90 * 100)), detail: `${name} · ${fmt(dated.length)} ${t("overviewExtraCertCount")}` };
}
if (id === "extra_cache_memory") return ramBudget(c.resident_bytes, c.hot_limit_bytes);
if (id === "extra_cold_queue_memory") return ramBudget(c.cold_queue_bytes, c.cold_queue_limit_bytes);
if (id === "extra_cold_storage") return ramBudget(c.cold_bytes, c.cold_limit_bytes);
if (!["extra_tls_error_rate", "extra_request_error_rate", "extra_cache_hit_rate", "extra_h2_share",
"extra_total_rps", "extra_h1_share", "extra_h3_share", "extra_cache_miss_rate",
"extra_error_share", "extra_rejected_rate"].includes(id))
return { value: null, unit: "count", detail: "" };
const fields = ({
extra_tls_error_rate: [m.tls_handshake_errors],
extra_request_error_rate: [m.errors],
extra_cache_hit_rate: [c.hits, c.misses],
extra_h2_share: [m.http2_requests, m.requests],
extra_h1_share: [m.http1_requests, m.requests],
extra_h3_share: [m.http3_requests, m.requests],
extra_total_rps: [m.requests],
extra_cache_miss_rate: [c.misses],
extra_error_share: [m.errors, m.requests],
extra_rejected_rate: [m.rejected_requests, m.rejected_connections],
})[id].map(positive);
const unit = ["extra_cache_hit_rate", "extra_h2_share", "extra_h1_share", "extra_h3_share", "extra_error_share"].includes(id)
? "percent" : "rate";
if (fields.some(v => v === null)) {
lastCounter.delete(id);
return { value: null, unit, detail: "", slices: null };
}
if (!record) return { value: lastValue.get(id) ?? null, unit, detail: "", slices: lastSegments.get(id) || null };
const result = elapsedDelta(id, fields, now);
if (!result) return { value: null, unit, detail: "", slices: null };
const { deltas, seconds } = result;
if (id === "extra_rejected_rate") return {
value: (deltas[0] + deltas[1]) / seconds, unit, detail: `${t("overviewExtraRejectedDetail")}: ${fmt(fields[0])} / ${fmt(fields[1])}`
};
if (id === "extra_error_share") return {
value: deltas[1] > 0 ? Math.min(100, deltas[0] / deltas[1] * 100) : 0, unit, detail: `${t("overviewExtraErrorShareDetail")}: ${fmt(deltas[0])} / ${fmt(deltas[1])}`
};
if (unit === "rate") return { value: deltas[0] / seconds, unit, detail: "" };
const sum = id === "extra_cache_hit_rate" ? deltas[0] + deltas[1] : deltas[1];
return {
value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "",
slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null
};
}
function format(value, unit) {
if (value == null) return "-";
if (unit === "percent") return `${fmt(value, 1)}%`;
if (unit === "bytes") return fmtBytes(value);
if (unit === "ms") return `${fmt(value, 1)} ms`;
if (unit === "rate") return `${fmt(value, 1)}/s`;
if (unit === "days") return `${fmt(value)} ${t("overviewExtraDaysUnit")}`;
if (unit === "text") return String(value);
return fmt(value);
}
function points(id, history, unit) {
if (!history.length) return "0,55 300,55";
const peak = window.rustProxyChartScale.peak(`extra:${id}`, unit, [history]);
return history.map((v, i) => `${i * 300 / Math.max(1, history.length - 1)},${55 - Math.min(v, peak) / peak * 50}`).join(" ");
}
function render(status, record = true) {
if (!status || currentPanel !== "overview") return;
const now = performance.now();
for (const id of names) {
const tile = node(id);
if (!tile) { lastCounter.delete(id); continue; }
const metric = read(id, status, now, record);
const value = metric.value;
if (lastUnits.get(id) !== metric.unit) {
histories.delete(id);
window.rustProxyChartScale.reset(`extra:${id}`);
lastUnits.set(id, metric.unit);
}
if (!bounded.has(id) && id !== "extra_cache_hit_rate") {
const history = histories.get(id) || [];
if (record && value !== null) {
history.push(value);
if (history.length > limit()) history.shift();
histories.set(id, history);
}
$(`overview-extra-${id}-line`)?.setAttribute("points", points(id, history, metric.unit));
window.rustProxyChartScale.axes(`overview-extra-${id}-line`,
window.rustProxyChartScale.peak(`extra:${id}`, metric.unit, [history]), metric.unit,
history.length, { top: 5 / 60, bottom: 55 / 60 });
}
if (record) {
lastValue.set(id, value);
if (id === "extra_cache_hit_rate") lastSegments.set(id, metric.slices || null);
}
const field = $(`overview-extra-${id}`);
if (field) field.textContent = format(value, metric.unit);
if (bounded.has(id)) {
meter(`overview-extra-${id}`, metric.meterPercent ?? (metric.unit === "percent" ? value : null));
} else if (id === "extra_cache_hit_rate") {
const slices = metric.slices || (record ? null : lastSegments.get(id));
donut("extra-cache-ratio", slices?.[0], slices?.[1]);
}
const detail = $(`overview-extra-${id}-detail`);
if (detail && id !== "extra_cache_hit_rate") {
const detailText = metric.detail || "";
detail.textContent = detailText;
detail.hidden = !detailText;
}
}
}
function reset() { histories.clear(); lastCounter.clear(); lastValue.clear(); lastUnits.clear(); lastSegments.clear(); window.rustProxyChartScale.reset("extra:"); }
window.rustProxyOverviewSamples?.onChange?.(() => {
for (const history of histories.values()) window.rustProxyOverviewSamples.trim(history);
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") render(latestStatus, false);
});
return { render, reset, meter, donut };
})();