236 lines
14 KiB
JavaScript
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 };
|
|
})();
|