Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions src/app/backups/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);

Expand All @@ -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.`}>
</PageTitle>
<div className="space-y-4">
{!backupData && <Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Backup data could not be loaded</AlertTitle>
<AlertDescription>
The configured backup storage could not be reached. Please verify your S3/B2 target settings and credentials.
</AlertDescription>
</Alert>}
{failedS3Targets.length > 0 && <Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Some S3 targets could not be loaded</AlertTitle>
<AlertDescription>
Backups from the following locations could not be fetched: {failedS3Targets.map((target) => `${target.name} (${target.endpoint}/${target.bucketName})`).join(', ')}
</AlertDescription>
</Alert>}
{backupsVolumesWithoutActualBackups.length > 0 && <Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Apps without Backup</AlertTitle>
Expand Down
17 changes: 12 additions & 5 deletions src/app/monitoring/disk-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)}%
</tspan>
<tspan
x={viewBox.cx}
Expand All @@ -94,7 +101,7 @@ export default function ChartDiskRessources({
x={viewBox.cx}
y={(viewBox.cy || 0) + 30}
className="fill-muted-foreground">
{KubeSizeConverter.convertBytesToReadableSize(nodeRessource.diskUsageAbsolut + nodeRessource.diskUsageReserved, 1, true)} / {KubeSizeConverter.convertBytesToReadableSize(nodeRessource.diskUsageCapacity, 1)}
{KubeSizeConverter.convertBytesToReadableSize(diskUsedAndReserved, 1, true)} / {KubeSizeConverter.convertBytesToReadableSize(diskCapacity, 1)}
</tspan>
</text>
);
Expand Down
34 changes: 25 additions & 9 deletions src/app/monitoring/monitoring-nodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<NodeResourceModel[] | undefined>(resourcesNodes);

const fetchResourcesNodes = async () => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -227,7 +232,7 @@ export default function ResourcesNodes({
return (
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
<tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-3xl font-bold">
{(((clusterStats.diskUsageAbsolut + clusterStats.diskUsageReserved) / clusterStats.diskCapacity) * 100).toFixed(0)}%
{toPercent(clusterStats.diskUsageAbsolut + clusterStats.diskUsageReserved, clusterStats.diskCapacity).toFixed(0)}%
</tspan>
<tspan x={viewBox.cx} y={(viewBox.cy || 0) + 24} className="fill-muted-foreground">
Used
Expand Down Expand Up @@ -283,11 +288,22 @@ export default function ResourcesNodes({
</TableCell>
<TableCell className="w-[25%]">
<div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground">
<span>{(((node.diskUsageAbsolut + node.diskUsageReserved) / node.diskUsageCapacity) * 100).toFixed(0)}%</span>
<span>{KubeSizeConverter.convertBytesToReadableSize(node.diskUsageAbsolut + node.diskUsageReserved)} / {KubeSizeConverter.convertBytesToReadableSize(node.diskUsageCapacity)}</span>
</div>
<Progress value={((node.diskUsageAbsolut + node.diskUsageReserved) / node.diskUsageCapacity) * 100} className="h-2" />
{(() => {
const diskUsed = getDiskUsageAbsolut(node);
const diskReserved = getDiskUsageReserved(node);
const diskCapacity = getDiskUsageCapacity(node);
const diskUsedAndReserved = diskUsed + diskReserved;
const diskPercent = toPercent(diskUsedAndReserved, diskCapacity);
return (
<>
<div className="flex justify-between text-xs text-muted-foreground">
<span>{diskPercent.toFixed(0)}%</span>
<span>{KubeSizeConverter.convertBytesToReadableSize(diskUsedAndReserved)} / {KubeSizeConverter.convertBytesToReadableSize(diskCapacity)}</span>
</div>
<Progress value={diskPercent} className="h-2" />
</>
);
})()}
</div>
</TableCell>
<TableCell className="text-right">
Expand Down
19 changes: 7 additions & 12 deletions src/app/monitoring/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
11 changes: 6 additions & 5 deletions src/server/services/cluster.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -88,18 +89,18 @@ 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!,
cpuUsage: latestUsageItem.cpuUsage,
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
}
}));
}
Expand Down
40 changes: 35 additions & 5 deletions src/server/services/standalone-services/backup.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,51 @@ 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);
}
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
};
}

Expand Down
8 changes: 4 additions & 4 deletions src/shared/model/node-resource.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof nodeResourceZodModel>;
15 changes: 15 additions & 0 deletions src/shared/utils/catch.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export class CatchUtils {
static async resultOrUndefined<T>(promiseFunc: () => Promise<T>, context?: string): Promise<T | undefined> {
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;
}
}
}
Loading