feat(api): add cached container cpu/mem fields on containers-dashboard
This commit is contained in:
@@ -70,6 +70,7 @@ async def _stats_poller_loop():
|
|||||||
key = _norm_container_name(st.get("Name"))
|
key = _norm_container_name(st.get("Name"))
|
||||||
if not key:
|
if not key:
|
||||||
continue
|
continue
|
||||||
|
# CPUPerc returned by Podman is already percentage (0.10 == 0.10%)
|
||||||
cpu_val = st.get("CPUPerc")
|
cpu_val = st.get("CPUPerc")
|
||||||
if cpu_val is None:
|
if cpu_val is None:
|
||||||
cpu_val = st.get("CPU")
|
cpu_val = st.get("CPU")
|
||||||
|
|||||||
+50
-46
@@ -501,9 +501,9 @@
|
|||||||
if (tab === "images") {
|
if (tab === "images") {
|
||||||
loadImages();
|
loadImages();
|
||||||
}
|
}
|
||||||
// Start/stop live stats alleen in Containers tab
|
// Start/stop live stats alleen in Containers tab (polling via /containers-dashboard)
|
||||||
if (tab === 'containers') startContainersStatsStream();
|
if (tab === 'containers') startContainersDashboardStatsPoll();
|
||||||
else stopContainersStatsStream();
|
else stopContainersDashboardStatsPoll();
|
||||||
|
|
||||||
refreshActive();
|
refreshActive();
|
||||||
}
|
}
|
||||||
@@ -526,9 +526,9 @@
|
|||||||
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
document.addEventListener("visibilitychange", () => {
|
||||||
if (document.visibilityState !== "visible") {
|
if (document.visibilityState !== "visible") {
|
||||||
stopContainersStatsStream();
|
stopContainersDashboardStatsPoll();
|
||||||
} else if (currentTab === "containers") {
|
} else if (currentTab === "containers") {
|
||||||
startContainersStatsStream();
|
startContainersDashboardStatsPoll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -814,58 +814,69 @@
|
|||||||
renderContainersGrouped(list, tbody, podStatus);
|
renderContainersGrouped(list, tbody, podStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
let containersStatsES = null;
|
let containersDashboardStatsTimer = null;
|
||||||
|
let containersDashboardStatsInFlight = false;
|
||||||
|
|
||||||
|
function startContainersDashboardStatsPoll() {
|
||||||
function startContainersStatsStream() {
|
if (containersDashboardStatsTimer) return;
|
||||||
if (containersStatsES) return;
|
// immediate tick, daarna elke 1s
|
||||||
|
pollContainersDashboardStatsOnce();
|
||||||
containersStatsES = new EventSource("/api/containers/stats/stream?interval=1");
|
containersDashboardStatsTimer = setInterval(pollContainersDashboardStatsOnce, 1000);
|
||||||
|
|
||||||
containersStatsES.addEventListener("stats", (ev) => {
|
|
||||||
let payload;
|
|
||||||
try {
|
|
||||||
payload = JSON.parse(ev.data);
|
|
||||||
} catch (e) {
|
|
||||||
// Slechte SSE chunk -> negeren zodat de stream niet "stilvalt"
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const statsList = payload?.data?.Stats || [];
|
|
||||||
// totals per pod voor deze SSE tick
|
function stopContainersDashboardStatsPoll() {
|
||||||
|
if (!containersDashboardStatsTimer) return;
|
||||||
|
clearInterval(containersDashboardStatsTimer);
|
||||||
|
containersDashboardStatsTimer = null;
|
||||||
|
resetPodTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollContainersDashboardStatsOnce() {
|
||||||
|
if (containersDashboardStatsInFlight) return;
|
||||||
|
containersDashboardStatsInFlight = true;
|
||||||
|
try {
|
||||||
|
const containers = await api('/containers-dashboard', 'GET');
|
||||||
|
const list = Array.isArray(containers) ? containers : (containers?.containers || []);
|
||||||
|
|
||||||
|
// totals per pod voor deze poll tick
|
||||||
const podCpu = new Map(); // podName -> cpuPct sum
|
const podCpu = new Map(); // podName -> cpuPct sum
|
||||||
const podMem = new Map(); // podName -> memBytes sum
|
const podMem = new Map(); // podName -> memBytes sum
|
||||||
const podMemPct = new Map(); // podName -> memPct sum
|
const podMemPct = new Map(); // podName -> memPct sum
|
||||||
|
|
||||||
for (const st of statsList) {
|
for (const c of (list || [])) {
|
||||||
const cname = normalizeContainerName(st?.Name);
|
const cname = normalizeContainerName((c?.Names && c.Names[0]) ? c.Names[0] : (c?.Names || c?.Name || c?.name || ''));
|
||||||
if (!cname) continue;
|
if (!cname) continue;
|
||||||
|
|
||||||
const key = cssSafeId(cname);
|
const key = cssSafeId(cname);
|
||||||
|
|
||||||
const cpuPct = Number(st?.CPUPerc ?? st?.CPU ?? st?.AvgCPU ?? 0);
|
const cpuRaw = c?._dashboard_cpu;
|
||||||
const memBytes = Number(st?.MemUsage ?? 0);
|
const memBytesRaw = c?._dashboard_mem_usage;
|
||||||
const memPct = Number(st?.MemPerc ?? 0);
|
const memPctRaw = c?._dashboard_mem_perc;
|
||||||
|
|
||||||
|
const cpuPct = Number(cpuRaw);
|
||||||
|
const memBytes = Number(memBytesRaw);
|
||||||
|
const memPct = Number(memPctRaw);
|
||||||
|
|
||||||
const pod = containersC2P.get(cname);
|
const pod = containersC2P.get(cname);
|
||||||
if (pod) {
|
if (pod) {
|
||||||
podCpu.set(pod, (podCpu.get(pod) || 0) + cpuPct);
|
if (Number.isFinite(cpuPct)) podCpu.set(pod, (podCpu.get(pod) || 0) + cpuPct);
|
||||||
podMem.set(pod, (podMem.get(pod) || 0) + memBytes);
|
if (Number.isFinite(memBytes)) podMem.set(pod, (podMem.get(pod) || 0) + memBytes);
|
||||||
podMemPct.set(pod, (podMemPct.get(pod) || 0) + memPct);
|
if (Number.isFinite(memPct)) podMemPct.set(pod, (podMemPct.get(pod) || 0) + memPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU: jouw API geeft CPU als fractie (0.00384 -> 0.384%)
|
|
||||||
const cpuEl = document.getElementById(`cpu-${key}`);
|
const cpuEl = document.getElementById(`cpu-${key}`);
|
||||||
if (cpuEl) cpuEl.textContent = cpuPct.toFixed(2) + "%";
|
if (cpuEl) cpuEl.textContent = Number.isFinite(cpuPct) ? (cpuPct.toFixed(2) + "%") : "-";
|
||||||
|
|
||||||
// MEM: bytes + percentage
|
|
||||||
const memEl = document.getElementById(`mem-${key}`);
|
const memEl = document.getElementById(`mem-${key}`);
|
||||||
if (memEl) {
|
if (memEl) {
|
||||||
const mem = formatBytes(memBytes);
|
if (Number.isFinite(memBytes) && Number.isFinite(memPct)) {
|
||||||
memEl.textContent = `${mem} (${memPct.toFixed(1)}%)`;
|
memEl.textContent = `${formatBytes(memBytes)} (${memPct.toFixed(1)}%)`;
|
||||||
|
} else {
|
||||||
|
memEl.textContent = "-";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// NA de container-loop: pod totals renderen
|
}
|
||||||
|
|
||||||
for (const [pod, cpuSum] of podCpu.entries()) {
|
for (const [pod, cpuSum] of podCpu.entries()) {
|
||||||
const el = document.getElementById(`podcpu-${cssSafeId(pod)}`);
|
const el = document.getElementById(`podcpu-${cssSafeId(pod)}`);
|
||||||
if (el) {
|
if (el) {
|
||||||
@@ -878,22 +889,15 @@
|
|||||||
const el = document.getElementById(`podmem-${cssSafeId(pod)}`);
|
const el = document.getElementById(`podmem-${cssSafeId(pod)}`);
|
||||||
if (el) {
|
if (el) {
|
||||||
const mp = podMemPct.get(pod) || 0;
|
const mp = podMemPct.get(pod) || 0;
|
||||||
el.textContent = `${formatBytes(memSum)} (${mp.toFixed(1)}%)`;
|
el.textContent = `${formatBytes(memSum)} (${Number(mp).toFixed(1)}%)`;
|
||||||
el.classList.remove('stale');
|
el.classList.remove('stale');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} catch (e) {
|
||||||
|
|
||||||
containersStatsES.onerror = () => {
|
|
||||||
resetPodTotals();
|
resetPodTotals();
|
||||||
};
|
} finally {
|
||||||
|
containersDashboardStatsInFlight = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopContainersStatsStream() {
|
|
||||||
if (!containersStatsES) return;
|
|
||||||
containersStatsES.close();
|
|
||||||
containersStatsES = null;
|
|
||||||
resetPodTotals();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetPodTotals() {
|
function resetPodTotals() {
|
||||||
|
|||||||
Reference in New Issue
Block a user