feat/add app monitoring usage

This commit is contained in:
biersoeckli
2025-01-09 10:47:50 +00:00
parent 487a635472
commit b743b387d5
21 changed files with 335 additions and 97 deletions

View File

@@ -3,6 +3,7 @@
import monitoringService from "@/server/services/monitoring.service";
import clusterService from "@/server/services/node.service";
import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils";
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
import { AppVolumeMonitoringUsageModel } from "@/shared/model/app-volume-monitoring-usage.model";
import { NodeResourceModel } from "@/shared/model/node-resource.model";
import { ServerActionResult } from "@/shared/model/server-action-error-return.model";
@@ -17,4 +18,10 @@ export const getVolumeMonitoringUsage = async () =>
simpleAction(async () => {
await getAuthUserSession();
return await monitoringService.getAllAppVolumesUsage();
}) as Promise<ServerActionResult<unknown, AppVolumeMonitoringUsageModel[]>>;
}) as Promise<ServerActionResult<unknown, AppVolumeMonitoringUsageModel[]>>;
export const getMonitoringForAllApps = async () =>
simpleAction(async () => {
await getAuthUserSession();
return await monitoringService.getMonitoringForAllApps();
}) as Promise<ServerActionResult<unknown, AppMonitoringUsageModel[]>>;

View File

@@ -0,0 +1,100 @@
'use client';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { useEffect, useState } from 'react';
import { Actions } from '@/frontend/utils/nextjs-actions.utils';
import { getMonitoringForAllApps, getVolumeMonitoringUsage } from './actions';
import { toast } from 'sonner';
import FullLoadingSpinner from '@/components/ui/full-loading-spinnter';
import { AppVolumeMonitoringUsageModel } from '@/shared/model/app-volume-monitoring-usage.model';
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { KubeSizeConverter } from '@/shared/utils/kubernetes-size-converter.utils';
import { ExternalLink } from 'lucide-react';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Progress } from "@/components/ui/progress"
import dataAccess from '@/server/adapter/db.client';
import { ProgressIndicator } from '@radix-ui/react-progress';
import { AppMonitoringUsageModel } from '@/shared/model/app-monitoring-usage.model';
export default function AppRessourceMonitoring({
appsRessourceUsage
}: {
appsRessourceUsage?: AppMonitoringUsageModel[]
}) {
const [updatedAppUsage, setUpdatedAppUsage] = useState<AppMonitoringUsageModel[] | undefined>(appsRessourceUsage);
const fetchMonitoringData = async () => {
try {
const data = await Actions.run(() => getMonitoringForAllApps());
setUpdatedAppUsage(data);
} catch (ex) {
toast.error('An error occurred while fetching current volume usage');
console.error('An error occurred while fetching volume nodes', ex);
}
}
useEffect(() => {
const intervalId = setInterval(() => fetchMonitoringData(), 10000);
return () => {
clearInterval(intervalId);
}
}, [appsRessourceUsage]);
if (!updatedAppUsage) {
return <Card>
<CardContent>
<FullLoadingSpinner />
</CardContent>
</Card>
}
return (
<Card>
<CardHeader>
<CardTitle>App Ressource Usage</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableCaption>{updatedAppUsage.length} Apps</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>App</TableHead>
<TableHead>CPU</TableHead>
<TableHead>RAM</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{updatedAppUsage.map((item, index) => (
<TableRow key={item.appId}>
<TableCell>{item.projectName}</TableCell>
<TableCell>{item.appName}</TableCell>
<TableCell>
<span className='font-semibold'>{item.cpuUsagePercent.toFixed(3)}%</span> / {item.cpuUsage.toFixed(5)} Cores
</TableCell>
<TableCell>{KubeSizeConverter.convertBytesToReadableSize(item.ramUsageBytes)}</TableCell>
<TableCell>
<Link href={`/project/app/${item.appId}`} >
<Button variant="ghost" size="sm">
<ExternalLink />
</Button>
</Link>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
}

View File

@@ -31,13 +31,12 @@ import { AppVolumeMonitoringUsageModel } from '@/shared/model/app-volume-monitor
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { KubeSizeConverter } from '@/shared/utils/kubernetes-size-converter.utils';
import AppVolumeMonitoring from './app-volumes-monitoring';
import AppRessourceMonitoring from './app-monitoring';
export default function ResourcesNodes({
resourcesNodes,
volumesUsage
}: {
resourcesNodes?: NodeResourceModel[];
volumesUsage?: AppVolumeMonitoringUsageModel[]
}) {
const [updatedNodeRessources, setUpdatedResourcesNodes] = useState<NodeResourceModel[] | undefined>(resourcesNodes);
@@ -248,7 +247,6 @@ export default function ResourcesNodes({
</Card>
</>))
}
<AppVolumeMonitoring volumesUsage={volumesUsage} />
</div >
</div>
);
}

View File

@@ -7,15 +7,22 @@ import ResourceNodes from "./monitoring-nodes";
import { NodeResourceModel } from "@/shared/model/node-resource.model";
import { AppVolumeMonitoringUsageModel } from "@/shared/model/app-volume-monitoring-usage.model";
import monitoringService from "@/server/services/monitoring.service";
import AppRessourceMonitoring from "./app-monitoring";
import AppVolumeMonitoring from "./app-volumes-monitoring";
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
export default async function ResourceNodesInfoPage() {
await getAuthUserSession();
let resourcesNode: NodeResourceModel[] | undefined;
let volumesUsage: AppVolumeMonitoringUsageModel[] | undefined;
let updatedNodeRessources: AppMonitoringUsageModel[] | undefined;
try {
resourcesNode = await clusterService.getNodeResourceUsage();
volumesUsage = await monitoringService.getAllAppVolumesUsage();
[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
}
@@ -26,7 +33,11 @@ export default async function ResourceNodesInfoPage() {
title={'Monitoring'}
subtitle={`View all resources of the nodes which belong to the QuickStack Cluster.`}>
</PageTitle>
<ResourceNodes resourcesNodes={resourcesNode} volumesUsage={volumesUsage} />
<div className="space-y-6">
<ResourceNodes resourcesNodes={resourcesNode} />
<AppRessourceMonitoring appsRessourceUsage={updatedNodeRessources} />
<AppVolumeMonitoring volumesUsage={volumesUsage} />
</div>
</div>
)
}

View File

@@ -55,7 +55,7 @@ export default function MonitoringTab({
<TableRow>
<TableCell className="font-medium">
<TooltipProvider>
<Tooltip>
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<div className={'px-3 py-1.5 rounded cursor-pointer'}>{selectedPod?.cpuPercent.toFixed(2)}</div>
</TooltipTrigger>

View File

@@ -14,12 +14,6 @@ export default async function NodeInfo({ nodeInfos }: { nodeInfos: NodeInfoModel
const { openConfirmDialog: openDialog } = useConfirmDialog();
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => setBreadcrumbs([
{ name: "Settings", url: "/settings/profile" },
{ name: "Cluster" },
]), []);
const setNodeStatusClick = async (nodeName: string, schedulable: boolean) => {
const confirmation = await openDialog({
title: 'Update Node Status',
@@ -87,7 +81,7 @@ export default async function NodeInfo({ nodeInfos }: { nodeInfos: NodeInfoModel
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className={nodeInfo.schedulable ? 'text-green-500 font-semibold' : 'text-red-500 font-semibold'}> {nodeInfo.schedulable ? 'Yes' : 'No'}</span>
<span className={nodeInfo.schedulable ? 'text-green-500 font-semibold' : 'text-red-500 font-semibold'}> {nodeInfo.schedulable ? 'Yes' : 'No'}</span>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[350px]">{nodeInfo.schedulable ? 'Node is ready to run containers.' : 'Node ist deactivated. All containers will be scheduled on other nodes.'}</p>

View File

@@ -7,6 +7,7 @@ import NodeInfo from "./nodeInfo";
import AddClusterNodeDialog from "./add-cluster-node-dialog";
import { Button } from "@/components/ui/button";
import paramService, { ParamService } from "@/server/services/param.service";
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
export default async function ClusterInfoPage() {
@@ -22,6 +23,10 @@ export default async function ClusterInfoPage() {
<Button>Add Cluster Node</Button>
</AddClusterNodeDialog>
</PageTitle>
<BreadcrumbSetter items={[
{ name: "Settings", url: "/settings/profile" },
{ name: "Cluster" },
]} />
<NodeInfo nodeInfos={nodeInfo} />
</div>
)

View File

@@ -0,0 +1,40 @@
'use server'
import { getAuthUserSession } from "@/server/utils/action-wrapper.utils";
import PageTitle from "@/components/custom/page-title";
import paramService, { ParamService } from "@/server/services/param.service";
import podService from "@/server/services/pod.service";
import { Constants } from "@/shared/utils/constants";
import s3TargetService from "@/server/services/s3-target.service";
import QuickStackVersionInfo from "./qs-version-info";
import QuickStackMaintenanceSettings from "./qs-maintenance-settings";
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
export default async function MaintenancePage() {
await getAuthUserSession();
const useCanaryChannel = await paramService.getBoolean(ParamService.USE_CANARY_CHANNEL, false);
const qsPodInfos = await podService.getPodsForApp(Constants.QS_NAMESPACE, Constants.QS_APP_NAME);
const qsPodInfo = qsPodInfos.find(p => !!p);
return (
<div className="flex-1 space-y-4 pt-6">
<PageTitle
title={'Maintenance'}
subtitle={`Options to maintain your QuickStack Cluster`}>
</PageTitle>
<BreadcrumbSetter items={[
{
name: 'Settings'
},
{
name: 'Maintenance'
},
]} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div><QuickStackVersionInfo useCanaryChannel={useCanaryChannel!} /></div>
<div><QuickStackMaintenanceSettings qsPodName={qsPodInfo?.podName} /></div>
</div>
</div>
)
}

View File

@@ -1,7 +1,7 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { purgeRegistryImages, updateQuickstack, updateRegistry } from "./actions";
import { cleanupOldBuildJobs, purgeRegistryImages, updateQuickstack, updateRegistry } from "../server/actions";
import { Button } from "@/components/ui/button";
import { Toast } from "@/frontend/utils/toast.utils";
import { useConfirmDialog } from "@/frontend/states/zustand.states";
@@ -17,17 +17,13 @@ export default function QuickStackMaintenanceSettings({
const useConfirm = useConfirmDialog();
return <>
return <div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Maintenance</CardTitle>
<CardTitle>Free Up Disk Space</CardTitle>
</CardHeader>
<CardContent className="flex gap-4 flex-wrap">
{qsPodName && <LogsDialog namespace={Constants.QS_NAMESPACE} podName={qsPodName}>
<Button variant="secondary" ><SquareTerminal /> Open QuickStack Logs</Button>
</LogsDialog>}
<Button variant="secondary" onClick={async () => {
if (await useConfirm.openConfirmDialog({
title: 'Purge Images',
@@ -38,6 +34,29 @@ export default function QuickStackMaintenanceSettings({
}
}}><Trash /> Purge Images</Button>
<Button variant="secondary" onClick={async () => {
if (await useConfirm.openConfirmDialog({
title: 'Cleanup Old Build Jobs',
description: 'This action deletes all old build jobs. Use this action to free up disk space.',
okButton: "Cleanup Old Build Jobs"
})) {
Toast.fromAction(() => cleanupOldBuildJobs());
}
}}><Trash /> Cleanup Old Build Jobs</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Monitoring & Troubleshooting</CardTitle>
</CardHeader>
<CardContent className="flex gap-4 flex-wrap">
{qsPodName && <LogsDialog namespace={Constants.QS_NAMESPACE} podName={qsPodName}>
<Button variant="secondary" ><SquareTerminal /> Open QuickStack Logs</Button>
</LogsDialog>}
<Button variant="secondary" onClick={async () => {
if (await useConfirm.openConfirmDialog({
title: 'Update Registry',
@@ -49,7 +68,6 @@ export default function QuickStackMaintenanceSettings({
}}><RotateCcw /> Force Update Registry</Button>
</CardContent>
</Card >
</>;
</Card>
</div>;
}

View File

@@ -1,7 +1,7 @@
'use client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { purgeRegistryImages, setCanaryChannel, updateQuickstack, updateRegistry } from "./actions";
import { purgeRegistryImages, setCanaryChannel, updateQuickstack, updateRegistry } from "../server/actions";
import { Button } from "@/components/ui/button";
import { Toast } from "@/frontend/utils/toast.utils";
import { useConfirmDialog } from "@/frontend/states/zustand.states";

View File

@@ -11,7 +11,7 @@ import PageTitle from "@/components/custom/page-title";
import ProfilePasswordChange from "./profile-password-change";
import ToTpSettings from "./totp-settings";
import userService from "@/server/services/user.service";
import BreadcrumbsSettings from "./profile-breadcrumbs";
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
export default async function ProjectPage() {
@@ -23,7 +23,10 @@ export default async function ProjectPage() {
title={'Profile'}
subtitle={`View or edit your Profile information and configure your authentication.`}>
</PageTitle>
<BreadcrumbsSettings />
<BreadcrumbSetter items={[
{ name: "Settings", url: "/settings/profile" },
{ name: "Profile" },
]} />
<ProfilePasswordChange />
<ToTpSettings totpEnabled={data.twoFaEnabled} />
</div>

View File

@@ -1,18 +0,0 @@
'use client';
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { deactivate2fa } from "./actions";
import { Toast } from "@/frontend/utils/toast.utils";
import TotpCreateDialog from "./totp-create-dialog";
import { Button } from "@/components/ui/button";
import { useBreadcrumbs } from "@/frontend/states/zustand.states";
import { useEffect } from "react";
export default function BreadcrumbsSettings() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => setBreadcrumbs([
{ name: "Settings", url: "/settings/profile" },
{ name: "Profile" },
]), []);
return <></>;
}

View File

@@ -6,6 +6,7 @@ import s3TargetService from "@/server/services/s3-target.service";
import S3TargetsTable from "./s3-targets-table";
import S3TargetEditOverlay from "./s3-target-edit-overlay";
import { Button } from "@/components/ui/button";
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
export default async function S3TargetsPage() {
@@ -21,6 +22,10 @@ export default async function S3TargetsPage() {
<Button>Add S3 Target</Button>
</S3TargetEditOverlay>
</PageTitle>
<BreadcrumbSetter items={[
{ name: "Settings", url: "/settings/profile" },
{ name: "S3 Targets" },
]} />
<S3TargetsTable targets={data} />
</div>
)

View File

@@ -12,6 +12,7 @@ import { Constants } from "@/shared/utils/constants";
import { QsPublicIpv4SettingsModel, qsPublicIpv4SettingsZodModel } from "@/shared/model/qs-public-ipv4-settings.model";
import ipAddressFinderAdapter from "@/server/adapter/ip-adress-finder.adapter";
import { KubeSizeConverter } from "@/shared/utils/kubernetes-size-converter.utils";
import buildService from "@/server/services/build.service";
export const updateIngressSettings = async (prevState: any, inputData: QsIngressSettingsModel) =>
saveFormAction(inputData, qsIngressSettingsZodModel, async (validatedData) => {
@@ -75,6 +76,13 @@ export const getConfiguredHostname: () => Promise<ServerActionResult<unknown, st
return await paramService.getString(ParamService.QS_SERVER_HOSTNAME);
});
export const cleanupOldBuildJobs = async () =>
simpleAction(async () => {
await getAuthUserSession();
await buildService.deleteAllFailedOrSuccededBuilds();
return new SuccessActionResult(undefined, 'Successfully cleaned up old build jobs.');
});
export const updateQuickstack = async () =>
simpleAction(async () => {
await getAuthUserSession();

View File

@@ -5,14 +5,11 @@ import PageTitle from "@/components/custom/page-title";
import paramService, { ParamService } from "@/server/services/param.service";
import QuickStackIngressSettings from "./qs-ingress-settings";
import QuickStackLetsEncryptSettings from "./qs-letsencrypt-settings";
import QuickStackMaintenanceSettings from "./qs-maintenance-settings";
import podService from "@/server/services/pod.service";
import { Constants } from "@/shared/utils/constants";
import ServerBreadcrumbs from "./server-breadcrumbs";
import QuickStackVersionInfo from "./qs-version-info";
import QuickStackRegistrySettings from "./qs-registry-settings";
import s3TargetService from "@/server/services/s3-target.service";
import QuickStackPublicIpSettings from "./qs-public-ip-settings";
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
export default async function ProjectPage() {
@@ -21,10 +18,7 @@ export default async function ProjectPage() {
const disableNodePortAccess = await paramService.getBoolean(ParamService.DISABLE_NODEPORT_ACCESS, false);
const letsEncryptMail = await paramService.getString(ParamService.LETS_ENCRYPT_MAIL, session.email);
const regitryStorageLocation = await paramService.getString(ParamService.REGISTRY_SOTRAGE_LOCATION, Constants.INTERNAL_REGISTRY_LOCATION);
const useCanaryChannel = await paramService.getBoolean(ParamService.USE_CANARY_CHANNEL, false);
const qsPodInfos = await podService.getPodsForApp(Constants.QS_NAMESPACE, Constants.QS_APP_NAME);
const ipv4Address = await paramService.getString(ParamService.PUBLIC_IPV4_ADDRESS);
const qsPodInfo = qsPodInfos.find(p => !!p);
const s3Targets = await s3TargetService.getAll();
return (
@@ -33,14 +27,15 @@ export default async function ProjectPage() {
title={'Server Settings'}
subtitle={`View or edit Server Settings`}>
</PageTitle>
<ServerBreadcrumbs />
<BreadcrumbSetter items={[
{ name: "Settings", url: "/settings/profile" },
{ name: "QuickStack Server" },
]} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div><QuickStackIngressSettings disableNodePortAccess={disableNodePortAccess!} serverUrl={serverUrl!} /></div>
<div> <QuickStackLetsEncryptSettings letsEncryptMail={letsEncryptMail!} /></div>
<div> <QuickStackPublicIpSettings publicIpv4={ipv4Address} /></div>
<div><QuickStackLetsEncryptSettings letsEncryptMail={letsEncryptMail!} /></div>
<div><QuickStackPublicIpSettings publicIpv4={ipv4Address} /></div>
<div><QuickStackRegistrySettings registryStorageLocation={regitryStorageLocation!} s3Targets={s3Targets} /></div>
<div><QuickStackMaintenanceSettings qsPodName={qsPodInfo?.podName} /></div>
<div><QuickStackVersionInfo useCanaryChannel={useCanaryChannel!} /></div>
</div>
</div>
)

View File

@@ -1,15 +0,0 @@
'use client';
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useBreadcrumbs } from "@/frontend/states/zustand.states";
import { useEffect } from "react";
export default function ServerBreadcrumbs() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => setBreadcrumbs([
{ name: "Settings", url: "/settings/profile" },
{ name: "QuickStack Server" },
]), []);
return <></>;
}

View File

@@ -39,13 +39,13 @@ const settingsMenu = [
icon: User,
},
{
title: "QuickStack Settings",
url: "/settings/server",
title: "S3 Targets",
url: "/settings/s3-targets",
icon: Settings,
},
{
title: "S3 Targets",
url: "/settings/s3-targets",
title: "QuickStack Settings",
url: "/settings/server",
icon: Settings,
},
{
@@ -53,6 +53,11 @@ const settingsMenu = [
url: "/settings/cluster",
icon: Server,
},
{
title: "Maintenance",
url: "/settings/maintenance",
icon: Settings,
},
]
export function SidebarCient({

View File

@@ -0,0 +1,15 @@
'use client';
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useEffect } from "react";
import { Breadcrumb, useBreadcrumbs } from "@/frontend/states/zustand.states";
export default function BreadcrumbSetter({ items }: { items: Breadcrumb[] }) {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs(items)
return () => setBreadcrumbs([]);
}, [items]);
return <></>;
}

View File

@@ -172,6 +172,17 @@ class BuildService {
}
}
async deleteAllFailedOrSuccededBuilds() {
const jobs = await k3s.batch.listNamespacedJob(BUILD_NAMESPACE);
const jobsToDelete = jobs.body.items.filter((job) => {
const status = this.getJobStatusString(job.status);
return status !== 'RUNNING';
});
for (const job of jobsToDelete) {
await this.deleteBuild(job.metadata?.name!);
}
}
async deleteAllBuildsOfProject(projectId: string) {
const jobs = await k3s.batch.listNamespacedJob(BUILD_NAMESPACE);
const jobsOfProject = jobs.body.items.filter((job) => job.metadata?.annotations?.[Constants.QS_ANNOTATION_PROJECT_ID] === projectId);

View File

@@ -9,6 +9,9 @@ import longhornApiAdapter from "../adapter/longhorn-api.adapter";
import dataAccess from "../adapter/db.client";
import pvcService from "./pvc.service";
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
import appService from "./app.service";
import projectService from "./project.service";
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
class MonitorService {
@@ -65,6 +68,43 @@ class MonitorService {
return appVolumesWithUsage;
}
async getMonitoringForAllApps() {
const [topPods, totalResourcesNodes, projects] = await Promise.all([
k8s.topPods(k3s.core, new k8s.Metrics(k3s.getKubeConfig())),
this.getTotalAvailableNodeRessources(),
projectService.getAllProjects()
]);
const appStats: AppMonitoringUsageModel[] = [];
for (let project of projects) {
for (let app of project.apps) {
const podsFromApp = await standalonePodService.getPodsForApp(project.id, app.id);
const filteredTopPods = topPods.filter((topPod) =>
podsFromApp.some((pod) => pod.podName === topPod.Pod.metadata?.name)
);
const totalResourcesApp = this.calulateTotalRessourceUsageOfApp(filteredTopPods);
const cpuUsagePercent = (totalResourcesApp.cpu / totalResourcesNodes.cpu) * 100;
appStats.push({
projectId: project.id,
projectName: project.name,
appName: app.name,
appId: app.id,
cpuUsage: totalResourcesApp.cpu,
cpuUsagePercent,
ramUsageBytes: totalResourcesApp.ramBytes
})
}
}
appStats.sort((a, b) => {
if (a.projectName === b.projectName) {
return a.appName.localeCompare(b.appName);
}
return a.projectName.localeCompare(b.projectName);
});
return appStats;
}
async getMonitoringForApp(projectId: string, appId: string): Promise<PodsResourceInfoModel> {
const metricsClient = new k8s.Metrics(k3s.getKubeConfig());
const podsFromApp = await standalonePodService.getPodsForApp(projectId, appId);
@@ -74,25 +114,8 @@ class MonitorService {
podsFromApp.some((pod) => pod.podName === topPod.Pod.metadata?.name)
);
const topNodes = await clusterService.getNodeInfo();
const totalResourcesNodes = topNodes.reduce(
(acc, node) => {
acc.cpu += Number(node.cpuCapacity) || 0;
acc.ramBytes += KubeSizeConverter.fromKubeSizeToBytes(node.ramCapacity) || 0;
return acc;
},
{ cpu: 0, ramBytes: 0 }
);
const totalResourcesApp = filteredTopPods.reduce(
(acc, pod) => {
acc.cpu += Number(pod.CPU.CurrentUsage) || 0;
acc.ramBytes += Number(pod.Memory.CurrentUsage) || 0;
return acc;
},
{ cpu: 0, ramBytes: 0 }
);
const totalResourcesNodes = await this.getTotalAvailableNodeRessources();
const totalResourcesApp = this.calulateTotalRessourceUsageOfApp(filteredTopPods);
var totalRamNodesCorrectUnit: number = totalResourcesNodes.ramBytes;
var totalRamAppCorrectUnit: number = totalResourcesApp.ramBytes;
@@ -108,6 +131,30 @@ class MonitorService {
}
}
private calulateTotalRessourceUsageOfApp(filteredTopPods: k8s.PodStatus[]) {
return filteredTopPods.reduce(
(acc, pod) => {
acc.cpu += Number(pod.CPU.CurrentUsage) || 0;
acc.ramBytes += Number(pod.Memory.CurrentUsage) || 0;
return acc;
},
{ cpu: 0, ramBytes: 0 }
);
}
private async getTotalAvailableNodeRessources() {
const topNodes = await clusterService.getNodeInfo();
const totalResourcesNodes = topNodes.reduce(
(acc, node) => {
acc.cpu += Number(node.cpuCapacity) || 0;
acc.ramBytes += KubeSizeConverter.fromKubeSizeToBytes(node.ramCapacity) || 0;
return acc;
},
{ cpu: 0, ramBytes: 0 }
);
return totalResourcesNodes;
}
async getPvcUsageFromApp(appId: string, projectId: string): Promise<Array<{ pvcName: string, usedBytes: number }>> {
const pvcFromApp = await pvcService.getAllPvcForApp(projectId, appId);
const pvcUsageData: Array<{ pvcName: string, usedBytes: number }> = [];

View File

@@ -0,0 +1,9 @@
export interface AppMonitoringUsageModel {
projectId: string,
projectName: string,
appName: string,
appId: string,
cpuUsage: number,
cpuUsagePercent: number,
ramUsageBytes: number
}