From b6b1349a7d1ed6ebc609bbb13205747b9f490410 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Sat, 4 Apr 2026 08:24:15 +0000 Subject: [PATCH] fix: implement error handling for external data fetching in backups and monitoring page --- src/app/backups/page.tsx | 24 +++++++++-- src/app/monitoring/disk-chart.tsx | 17 +++++--- src/app/monitoring/monitoring-nodes.tsx | 34 +++++++++++----- src/app/monitoring/page.tsx | 19 ++++----- src/server/services/cluster.service.ts | 11 ++--- .../standalone-services/backup.service.ts | 40 ++++++++++++++++--- src/shared/model/node-resource.model.ts | 8 ++-- src/shared/utils/catch.utils.ts | 15 +++++++ 8 files changed, 124 insertions(+), 44 deletions(-) create mode 100644 src/shared/utils/catch.utils.ts diff --git a/src/app/backups/page.tsx b/src/app/backups/page.tsx index 27d8459..84d1ac4 100644 --- a/src/app/backups/page.tsx +++ b/src/app/backups/page.tsx @@ -4,6 +4,7 @@ import { getAuthUserSession, isAuthorizedForBackups } from "@/server/utils/actio import PageTitle from "@/components/custom/page-title"; import backupService from "@/server/services/standalone-services/backup.service"; import BackupsTable from "./backups-table"; +import { CatchUtils } from "@/shared/utils/catch.utils"; import { AlertCircle, AlertTriangleIcon } from "lucide-react" import { Alert, @@ -15,10 +16,11 @@ import { export default async function BackupsPage() { await isAuthorizedForBackups(); - const { - backupInfoModels, - backupsVolumesWithoutActualBackups - } = await backupService.getBackupsForAllS3Targets(); + const backupData = await CatchUtils.resultOrUndefined(() => backupService.getBackupsForAllS3Targets()); + + const backupInfoModels = backupData?.backupInfoModels ?? []; + const backupsVolumesWithoutActualBackups = backupData?.backupsVolumesWithoutActualBackups ?? []; + const failedS3Targets = backupData?.failedS3Targets ?? []; const hasMissedBackups = backupInfoModels.some(x => x.missedBackup === true); @@ -29,6 +31,20 @@ export default async function BackupsPage() { subtitle={`View all backups wich are stored in all S3 Target destinations. If a backup exists from an app wich doesnt exist anymore, it will be shown as orphaned.`}>
+ {!backupData && + + Backup data could not be loaded + + The configured backup storage could not be reached. Please verify your S3/B2 target settings and credentials. + + } + {failedS3Targets.length > 0 && + + Some S3 targets could not be loaded + + Backups from the following locations could not be fetched: {failedS3Targets.map((target) => `${target.name} (${target.endpoint}/${target.bucketName})`).join(', ')} + + } {backupsVolumesWithoutActualBackups.length > 0 && Apps without Backup diff --git a/src/app/monitoring/disk-chart.tsx b/src/app/monitoring/disk-chart.tsx index 3219e92..f8727ef 100644 --- a/src/app/monitoring/disk-chart.tsx +++ b/src/app/monitoring/disk-chart.tsx @@ -17,10 +17,17 @@ export default function ChartDiskRessources({ nodeRessource: NodeResourceModel; }) { + const diskUsed = nodeRessource.diskUsageAbsolut ?? 0; + const diskReserved = nodeRessource.diskUsageReserved ?? 0; + const diskCapacity = nodeRessource.diskUsageCapacity ?? 0; + const diskSchedulable = nodeRessource.diskSpaceSchedulable ?? Math.max(0, diskCapacity - diskUsed - diskReserved); + const diskUsedAndReserved = diskUsed + diskReserved; + const diskUsagePercent = diskCapacity > 0 ? (diskUsedAndReserved / diskCapacity) * 100 : 0; + const chartData = [{ - diskUsed: nodeRessource.diskUsageAbsolut, - diskReserved: nodeRessource.diskUsageReserved, - diskSchedulable: nodeRessource.diskSpaceSchedulable + diskUsed, + diskReserved, + diskSchedulable }]; const chartConfig = { @@ -81,7 +88,7 @@ export default function ChartDiskRessources({ y={(viewBox.cy || 0) - 10} className="fill-foreground text-4xl font-bold" > - {((nodeRessource.diskUsageAbsolut + nodeRessource.diskUsageReserved) / nodeRessource.diskUsageCapacity * 100).toFixed(0)}% + {diskUsagePercent.toFixed(0)}% - {KubeSizeConverter.convertBytesToReadableSize(nodeRessource.diskUsageAbsolut + nodeRessource.diskUsageReserved, 1, true)} / {KubeSizeConverter.convertBytesToReadableSize(nodeRessource.diskUsageCapacity, 1)} + {KubeSizeConverter.convertBytesToReadableSize(diskUsedAndReserved, 1, true)} / {KubeSizeConverter.convertBytesToReadableSize(diskCapacity, 1)} ); diff --git a/src/app/monitoring/monitoring-nodes.tsx b/src/app/monitoring/monitoring-nodes.tsx index 19cdb78..33cf351 100644 --- a/src/app/monitoring/monitoring-nodes.tsx +++ b/src/app/monitoring/monitoring-nodes.tsx @@ -40,6 +40,11 @@ export default function ResourcesNodes({ resourcesNodes?: NodeResourceModel[]; }) { + const getDiskUsageAbsolut = (node: NodeResourceModel) => node.diskUsageAbsolut ?? 0; + const getDiskUsageReserved = (node: NodeResourceModel) => node.diskUsageReserved ?? 0; + const getDiskUsageCapacity = (node: NodeResourceModel) => node.diskUsageCapacity ?? 0; + const toPercent = (used: number, capacity: number) => (capacity > 0 ? (used / capacity) * 100 : 0); + const [updatedNodeRessources, setUpdatedResourcesNodes] = useState(resourcesNodes); const fetchResourcesNodes = async () => { @@ -76,9 +81,9 @@ export default function ResourcesNodes({ cpuCapacity: acc.cpuCapacity + node.cpuCapacity, ramUsage: acc.ramUsage + node.ramUsage, ramCapacity: acc.ramCapacity + node.ramCapacity, - diskUsageAbsolut: acc.diskUsageAbsolut + node.diskUsageAbsolut, - diskUsageReserved: acc.diskUsageReserved + node.diskUsageReserved, - diskCapacity: acc.diskCapacity + node.diskUsageCapacity, + diskUsageAbsolut: acc.diskUsageAbsolut + getDiskUsageAbsolut(node), + diskUsageReserved: acc.diskUsageReserved + getDiskUsageReserved(node), + diskCapacity: acc.diskCapacity + getDiskUsageCapacity(node), }), { cpuUsage: 0, cpuCapacity: 0, ramUsage: 0, ramCapacity: 0, @@ -227,7 +232,7 @@ export default function ResourcesNodes({ return ( - {(((clusterStats.diskUsageAbsolut + clusterStats.diskUsageReserved) / clusterStats.diskCapacity) * 100).toFixed(0)}% + {toPercent(clusterStats.diskUsageAbsolut + clusterStats.diskUsageReserved, clusterStats.diskCapacity).toFixed(0)}% Used @@ -283,11 +288,22 @@ export default function ResourcesNodes({
-
- {(((node.diskUsageAbsolut + node.diskUsageReserved) / node.diskUsageCapacity) * 100).toFixed(0)}% - {KubeSizeConverter.convertBytesToReadableSize(node.diskUsageAbsolut + node.diskUsageReserved)} / {KubeSizeConverter.convertBytesToReadableSize(node.diskUsageCapacity)} -
- + {(() => { + const diskUsed = getDiskUsageAbsolut(node); + const diskReserved = getDiskUsageReserved(node); + const diskCapacity = getDiskUsageCapacity(node); + const diskUsedAndReserved = diskUsed + diskReserved; + const diskPercent = toPercent(diskUsedAndReserved, diskCapacity); + return ( + <> +
+ {diskPercent.toFixed(0)}% + {KubeSizeConverter.convertBytesToReadableSize(diskUsedAndReserved)} / {KubeSizeConverter.convertBytesToReadableSize(diskCapacity)} +
+ + + ); + })()}
diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index 4958008..271cf74 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -11,22 +11,17 @@ import AppRessourceMonitoring from "./app-monitoring"; import AppVolumeMonitoring from "./app-volumes-monitoring"; import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model"; import { UserGroupUtils } from "@/shared/utils/role.utils"; +import { CatchUtils } from "@/shared/utils/catch.utils"; export default async function ResourceNodesInfoPage() { const session = await getAuthUserSession(); - let resourcesNode: NodeResourceModel[] | undefined; - let volumesUsage: AppVolumeMonitoringUsageModel[] | undefined; - let updatedNodeRessources: AppMonitoringUsageModel[] | undefined; - try { - [resourcesNode, volumesUsage, updatedNodeRessources] = await Promise.all([ - clusterService.getNodeResourceUsage(), - monitoringService.getAllAppVolumesUsage(), - await monitoringService.getMonitoringForAllApps() - ]); - } catch (ex) { - // do nothing --> if an error occurs, the ResourceNodes will show a loading spinner and error message - } + + let [resourcesNode, volumesUsage, updatedNodeRessources] = await Promise.all([ + CatchUtils.resultOrUndefined(() => clusterService.getNodeResourceUsage()), + CatchUtils.resultOrUndefined(() => monitoringService.getAllAppVolumesUsage()), + CatchUtils.resultOrUndefined(() => monitoringService.getMonitoringForAllApps()) + ]); // filter by role volumesUsage = volumesUsage?.filter((volume) => UserGroupUtils.sessionHasReadAccessForApp(session, volume.appId)); diff --git a/src/server/services/cluster.service.ts b/src/server/services/cluster.service.ts index 0dbf487..416a009 100644 --- a/src/server/services/cluster.service.ts +++ b/src/server/services/cluster.service.ts @@ -6,6 +6,7 @@ import { Tags } from "../utils/cache-tag-generator.utils"; import { revalidateTag, unstable_cache } from "next/cache"; import longhornApiAdapter from "../adapter/longhorn-api.adapter"; import { KubeSizeConverter } from "../../shared/utils/kubernetes-size-converter.utils"; +import { CatchUtils } from "@/shared/utils/catch.utils"; class ClusterService { @@ -88,7 +89,7 @@ class ClusterService { nodeMetrics.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); const latestUsageItem = nodeMetrics[0]; - const diskInfo = await longhornApiAdapter.getNodeStorageInfo(node.Node.metadata?.name!); + const diskInfo = await CatchUtils.resultOrUndefined(() => longhornApiAdapter.getNodeStorageInfo(node.Node.metadata?.name!)); return { name: node.Node.metadata?.name!, @@ -96,10 +97,10 @@ class ClusterService { cpuCapacity: Number(node.CPU?.Capacity!), ramUsage: latestUsageItem.ramUsage, ramCapacity: Number(node.Memory?.Capacity!), - diskUsageAbsolut: diskInfo.totalStorageMaximum - diskInfo.totalStorageAvailable, - diskUsageReserved: diskInfo.totalStorageReserved, - diskUsageCapacity: diskInfo.totalStorageMaximum, - diskSpaceSchedulable: diskInfo.totalSchedulableStorage + diskUsageAbsolut: diskInfo ? diskInfo.totalStorageMaximum - diskInfo.totalStorageAvailable : undefined, + diskUsageReserved: diskInfo?.totalStorageReserved, + diskUsageCapacity: diskInfo?.totalStorageMaximum, + diskSpaceSchedulable: diskInfo?.totalSchedulableStorage } })); } diff --git a/src/server/services/standalone-services/backup.service.ts b/src/server/services/standalone-services/backup.service.ts index 0d8bc5d..1be0878 100644 --- a/src/server/services/standalone-services/backup.service.ts +++ b/src/server/services/standalone-services/backup.service.ts @@ -100,10 +100,39 @@ class BackupService { async getBackupsForAllS3Targets() { const s3Targets = await dataAccess.client.s3Target.findMany(); - const returnValFromAllS3Targets = await Promise.all(s3Targets.map(s3Target => - this.getBackupsFromS3Target(s3Target))); + const returnValFromAllS3Targets = await Promise.all(s3Targets.map(async (s3Target) => { + try { + const data = await this.getBackupsFromS3Target(s3Target); + return { + s3Target, + data, + error: undefined + }; + } catch (error) { + const errorMessage = error instanceof Error + ? `${error.name}: ${error.message}` + : String(error); + console.error(`Failed to fetch backups for S3 target '${s3Target.name}' (${s3Target.endpoint}/${s3Target.bucketName})`, error); + return { + s3Target, + data: undefined, + error: errorMessage + }; + } + })); + + const successfulResults = returnValFromAllS3Targets.filter((result) => !!result.data); + const failedS3Targets = returnValFromAllS3Targets + .filter((result) => !!result.error) + .map((result) => ({ + id: result.s3Target.id, + name: result.s3Target.name, + endpoint: result.s3Target.endpoint, + bucketName: result.s3Target.bucketName, + error: result.error ?? 'Unknown error' + })); - const backupInfoModels = returnValFromAllS3Targets.map(x => x.backupInfoModels).flat(); + const backupInfoModels = successfulResults.map(x => x.data!.backupInfoModels).flat(); backupInfoModels.sort((a, b) => { if (a.projectName === b.projectName) { return a.appName.localeCompare(b.appName); @@ -111,10 +140,11 @@ class BackupService { return a.projectName.localeCompare(b.projectName); }); - const backupsVolumesWithoutActualBackups = returnValFromAllS3Targets.map(x => x.backupsVolumesWithoutActualBackups).flat(); + const backupsVolumesWithoutActualBackups = successfulResults.map(x => x.data!.backupsVolumesWithoutActualBackups).flat(); return { backupInfoModels, - backupsVolumesWithoutActualBackups + backupsVolumesWithoutActualBackups, + failedS3Targets }; } diff --git a/src/shared/model/node-resource.model.ts b/src/shared/model/node-resource.model.ts index 7aa449e..8c2e874 100644 --- a/src/shared/model/node-resource.model.ts +++ b/src/shared/model/node-resource.model.ts @@ -8,10 +8,10 @@ export const nodeResourceZodModel = z.object({ cpuCapacity: z.number(), ramUsage: z.number(), ramCapacity: z.number(), - diskUsageAbsolut: z.number(), - diskUsageCapacity: z.number(), - diskUsageReserved: z.number(), - diskSpaceSchedulable: z.number(), + diskUsageAbsolut: z.number().optional(), + diskUsageCapacity: z.number().optional(), + diskUsageReserved: z.number().optional(), + diskSpaceSchedulable: z.number().optional(), }) export type NodeResourceModel = z.infer; \ No newline at end of file diff --git a/src/shared/utils/catch.utils.ts b/src/shared/utils/catch.utils.ts new file mode 100644 index 0000000..ac2553a --- /dev/null +++ b/src/shared/utils/catch.utils.ts @@ -0,0 +1,15 @@ +export class CatchUtils { + static async resultOrUndefined(promiseFunc: () => Promise, context?: string): Promise { + const promiseName = promiseFunc.name || 'anonymous-promise'; + const contextInfo = context ? ` [${context}]` : ''; + try { + return await promiseFunc(); + } catch (error) { + const errorInfo = error instanceof Error + ? `${error.name}: ${error.message}` + : String(error); + console.error(`Error in resultOrUndefined${contextInfo} (${promiseName}): ${errorInfo}`, error); + return undefined; + } + } +} \ No newline at end of file