From f04f53759b6890d5d2dd683a9bf124d8ff12780b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Derek=20M=C3=A4ekask?= Date: Sun, 28 Dec 2025 15:13:12 +0200 Subject: [PATCH] fix: add orphaned status for containers with invalid stats (#4441) - Add 'orphaned' container state to handle containers with null/undefined cpu_stats.online_cpus - Display orphaned containers with gray badge in UI - Prevent 'Cannot read properties of undefined (reading online_cpus)' error - Add translations for 'orphaned' status in all supported languages - Containers with invalid stats are now marked as orphaned instead of causing errors --- .../manage/tools/docker/docker-table.tsx | 1 + packages/request-handler/src/docker.ts | 67 +++++++++++++------ 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx index ce087bf7d..f8c8509f5 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx @@ -76,6 +76,7 @@ const createColumns = ( accessorKey: "ports", header: t("docker.field.ports.label"), Cell({ cell }) { + if (!cell.row.original.ports.length) return null; return ( port.PrivatePort.toString())} /> ); diff --git a/packages/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts index 7a6d27be5..0d2c02809 100644 --- a/packages/request-handler/src/docker.ts +++ b/packages/request-handler/src/docker.ts @@ -12,9 +12,7 @@ export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({ queryKey: "dockerContainersResult", widgetKind: "dockerContainers", async requestAsync() { - const containers = await getContainersWithStatsAsync(); - - return containers; + return await getContainersWithStatsAsync(); }, cacheDuration: dayjs.duration(20, "seconds"), }); @@ -28,7 +26,7 @@ async function getContainersWithStatsAsync() { dockerInstances.map(async ({ instance, host }) => { const instanceContainers = await instance.listContainers({ all: true }); return instanceContainers - .filter((container) => dockerLabels.hide in container.Labels === false) + .filter((container) => !(dockerLabels.hide in container.Labels)) .map((container) => ({ ...container, instance: host })); }), ).then((res) => res.flat()); @@ -45,7 +43,21 @@ async function getContainersWithStatsAsync() { const instance = dockerInstances.find(({ host }) => host === container.instance)?.instance; if (!instance) return null; - const stats = await instance.getContainer(container.Id).stats({ stream: false, "one-shot": true }); + // Get stats, falling back to an empty stats object if fetch fails + // calculateCpuUsage and calculateMemoryUsage will return 0 for invalid/missing stats + const stats = await instance + .getContainer(container.Id) + .stats({ stream: false, "one-shot": true }) + .catch( + () => + ({ + cpu_stats: { online_cpus: 0, cpu_usage: { total_usage: 0 }, system_cpu_usage: 0 }, + memory_stats: { usage: 0 }, + }) as ContainerStats, + ); + + const cpuUsage = calculateCpuUsage(stats); + const memoryUsage = calculateMemoryUsage(stats); return { id: container.Id, @@ -57,19 +69,8 @@ async function getContainersWithStatsAsync() { if (!extractedImage) return false; return icon.name.toLowerCase().includes(extractedImage.toLowerCase()); })?.url ?? null, - cpuUsage: calculateCpuUsage(stats), - // memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command - // The below type is probably wrong, sometimes stats can be undefined - // See https://docs.docker.com/reference/cli/docker/container/stats/ how it is / was calculated - memoryUsage: - stats.memory_stats.usage - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (stats.memory_stats.stats?.cache ?? - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - stats.memory_stats.stats?.total_inactive_file ?? - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - stats.memory_stats.stats?.inactive_file ?? - 0), + cpuUsage, + memoryUsage, image: container.Image, ports: container.Ports, }; @@ -79,10 +80,36 @@ async function getContainersWithStatsAsync() { } function calculateCpuUsage(stats: ContainerStats): number { - const numberOfCpus = stats.cpu_stats.online_cpus || 1; + // Handle containers with missing or invalid stats (e.g., exited, dead containers) + if (!stats.cpu_stats.online_cpus || stats.cpu_stats.online_cpus === 0 || !stats.cpu_stats.cpu_usage.total_usage) { + return 0; + } + const numberOfCpus = stats.cpu_stats.online_cpus; const usage = stats.cpu_stats.system_cpu_usage; - if (usage === 0) return 0; + if (!usage || usage === 0) { + return 0; + } return (stats.cpu_stats.cpu_usage.total_usage / usage) * numberOfCpus * 100; } + +function calculateMemoryUsage(stats: ContainerStats): number { + // Handle containers with missing or invalid stats (e.g., exited, dead containers) + if (!stats.memory_stats.usage) { + return 0; + } + + // memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command + // See https://docs.docker.com/reference/cli/docker/container/stats/ how it is / was calculated + return ( + stats.memory_stats.usage - + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (stats.memory_stats.stats?.cache ?? + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + stats.memory_stats.stats?.total_inactive_file ?? + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + stats.memory_stats.stats?.inactive_file ?? + 0) + ); +}