From b743b387d5b71b95a395b0a49f87e7d86284075d Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Thu, 9 Jan 2025 10:47:50 +0000 Subject: [PATCH] feat/add app monitoring usage --- src/app/monitoring/actions.ts | 9 +- src/app/monitoring/app-monitoring.tsx | 100 ++++++++++++++++++ src/app/monitoring/monitoring-nodes.tsx | 6 +- src/app/monitoring/page.tsx | 17 ++- .../app/[appId]/overview/monitoring-app.tsx | 2 +- src/app/settings/cluster/nodeInfo.tsx | 8 +- src/app/settings/cluster/page.tsx | 5 + src/app/settings/maintenance/page.tsx | 40 +++++++ .../qs-maintenance-settings.tsx | 38 +++++-- .../qs-version-info.tsx | 2 +- src/app/settings/profile/page.tsx | 7 +- .../settings/profile/profile-breadcrumbs.tsx | 18 ---- src/app/settings/s3-targets/page.tsx | 5 + src/app/settings/server/actions.ts | 8 ++ src/app/settings/server/page.tsx | 19 ++-- .../settings/server/server-breadcrumbs.tsx | 15 --- src/app/sidebar-client.tsx | 13 ++- src/components/breadcrumbs-setter.tsx | 15 +++ src/server/services/build.service.ts | 11 ++ src/server/services/monitoring.service.ts | 85 +++++++++++---- .../model/app-monitoring-usage.model.ts | 9 ++ 21 files changed, 335 insertions(+), 97 deletions(-) create mode 100644 src/app/monitoring/app-monitoring.tsx create mode 100644 src/app/settings/maintenance/page.tsx rename src/app/settings/{server => maintenance}/qs-maintenance-settings.tsx (67%) rename src/app/settings/{server => maintenance}/qs-version-info.tsx (98%) delete mode 100644 src/app/settings/profile/profile-breadcrumbs.tsx delete mode 100644 src/app/settings/server/server-breadcrumbs.tsx create mode 100644 src/components/breadcrumbs-setter.tsx create mode 100644 src/shared/model/app-monitoring-usage.model.ts diff --git a/src/app/monitoring/actions.ts b/src/app/monitoring/actions.ts index 887b805..7b48011 100644 --- a/src/app/monitoring/actions.ts +++ b/src/app/monitoring/actions.ts @@ -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>; \ No newline at end of file + }) as Promise>; + +export const getMonitoringForAllApps = async () => + simpleAction(async () => { + await getAuthUserSession(); + return await monitoringService.getMonitoringForAllApps(); + }) as Promise>; \ No newline at end of file diff --git a/src/app/monitoring/app-monitoring.tsx b/src/app/monitoring/app-monitoring.tsx new file mode 100644 index 0000000..0173701 --- /dev/null +++ b/src/app/monitoring/app-monitoring.tsx @@ -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(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 + + + + + } + + return ( + + + App Ressource Usage + + + + {updatedAppUsage.length} Apps + + + Project + App + CPU + RAM + + + + + {updatedAppUsage.map((item, index) => ( + + {item.projectName} + {item.appName} + + {item.cpuUsagePercent.toFixed(3)}% / {item.cpuUsage.toFixed(5)} Cores + + {KubeSizeConverter.convertBytesToReadableSize(item.ramUsageBytes)} + + + + + + + ))} + +
+
+
+ ); +} diff --git a/src/app/monitoring/monitoring-nodes.tsx b/src/app/monitoring/monitoring-nodes.tsx index c2cac8d..093fb60 100644 --- a/src/app/monitoring/monitoring-nodes.tsx +++ b/src/app/monitoring/monitoring-nodes.tsx @@ -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(resourcesNodes); @@ -248,7 +247,6 @@ export default function ResourcesNodes({ )) } - - + ); } diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index 98dcc4c..2ed92fc 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -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.`}> - +
+ + + +
) } diff --git a/src/app/project/app/[appId]/overview/monitoring-app.tsx b/src/app/project/app/[appId]/overview/monitoring-app.tsx index 790d2a7..73c7273 100644 --- a/src/app/project/app/[appId]/overview/monitoring-app.tsx +++ b/src/app/project/app/[appId]/overview/monitoring-app.tsx @@ -55,7 +55,7 @@ export default function MonitoringTab({ - +
{selectedPod?.cpuPercent.toFixed(2)}
diff --git a/src/app/settings/cluster/nodeInfo.tsx b/src/app/settings/cluster/nodeInfo.tsx index c0c67af..8b9a643 100644 --- a/src/app/settings/cluster/nodeInfo.tsx +++ b/src/app/settings/cluster/nodeInfo.tsx @@ -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 - {nodeInfo.schedulable ? 'Yes' : 'No'} + {nodeInfo.schedulable ? 'Yes' : 'No'}

{nodeInfo.schedulable ? 'Node is ready to run containers.' : 'Node ist deactivated. All containers will be scheduled on other nodes.'}

diff --git a/src/app/settings/cluster/page.tsx b/src/app/settings/cluster/page.tsx index 2e36fac..1ccdef9 100644 --- a/src/app/settings/cluster/page.tsx +++ b/src/app/settings/cluster/page.tsx @@ -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() { + ) diff --git a/src/app/settings/maintenance/page.tsx b/src/app/settings/maintenance/page.tsx new file mode 100644 index 0000000..2e89573 --- /dev/null +++ b/src/app/settings/maintenance/page.tsx @@ -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 ( +
+ + + +
+
+
+
+
+ ) +} diff --git a/src/app/settings/server/qs-maintenance-settings.tsx b/src/app/settings/maintenance/qs-maintenance-settings.tsx similarity index 67% rename from src/app/settings/server/qs-maintenance-settings.tsx rename to src/app/settings/maintenance/qs-maintenance-settings.tsx index f260c34..a9b5057 100644 --- a/src/app/settings/server/qs-maintenance-settings.tsx +++ b/src/app/settings/maintenance/qs-maintenance-settings.tsx @@ -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
- Maintenance + Free Up Disk Space - {qsPodName && - - } - + + + + + + + + Monitoring & Troubleshooting + + + + {qsPodName && + + } + - - - ; + +
; } \ No newline at end of file diff --git a/src/app/settings/server/qs-version-info.tsx b/src/app/settings/maintenance/qs-version-info.tsx similarity index 98% rename from src/app/settings/server/qs-version-info.tsx rename to src/app/settings/maintenance/qs-version-info.tsx index 931cc5e..7f2f3c3 100644 --- a/src/app/settings/server/qs-version-info.tsx +++ b/src/app/settings/maintenance/qs-version-info.tsx @@ -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"; diff --git a/src/app/settings/profile/page.tsx b/src/app/settings/profile/page.tsx index 976368f..338cb7f 100644 --- a/src/app/settings/profile/page.tsx +++ b/src/app/settings/profile/page.tsx @@ -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.`}> - + diff --git a/src/app/settings/profile/profile-breadcrumbs.tsx b/src/app/settings/profile/profile-breadcrumbs.tsx deleted file mode 100644 index 7f04fb2..0000000 --- a/src/app/settings/profile/profile-breadcrumbs.tsx +++ /dev/null @@ -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 <>; -} \ No newline at end of file diff --git a/src/app/settings/s3-targets/page.tsx b/src/app/settings/s3-targets/page.tsx index eec631a..9b7a3f7 100644 --- a/src/app/settings/s3-targets/page.tsx +++ b/src/app/settings/s3-targets/page.tsx @@ -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() { + ) diff --git a/src/app/settings/server/actions.ts b/src/app/settings/server/actions.ts index 1fbbf26..83a24a1 100644 --- a/src/app/settings/server/actions.ts +++ b/src/app/settings/server/actions.ts @@ -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 + 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(); diff --git a/src/app/settings/server/page.tsx b/src/app/settings/server/page.tsx index 30aa77d..1ba0bcc 100644 --- a/src/app/settings/server/page.tsx +++ b/src/app/settings/server/page.tsx @@ -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`}> - +
-
-
+
+
-
-
) diff --git a/src/app/settings/server/server-breadcrumbs.tsx b/src/app/settings/server/server-breadcrumbs.tsx deleted file mode 100644 index ee6cadd..0000000 --- a/src/app/settings/server/server-breadcrumbs.tsx +++ /dev/null @@ -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 <>; -} \ No newline at end of file diff --git a/src/app/sidebar-client.tsx b/src/app/sidebar-client.tsx index af915b6..c8c08de 100644 --- a/src/app/sidebar-client.tsx +++ b/src/app/sidebar-client.tsx @@ -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({ diff --git a/src/components/breadcrumbs-setter.tsx b/src/components/breadcrumbs-setter.tsx new file mode 100644 index 0000000..13234d6 --- /dev/null +++ b/src/components/breadcrumbs-setter.tsx @@ -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 <>; +} \ No newline at end of file diff --git a/src/server/services/build.service.ts b/src/server/services/build.service.ts index 903dd0c..f56b45b 100644 --- a/src/server/services/build.service.ts +++ b/src/server/services/build.service.ts @@ -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); diff --git a/src/server/services/monitoring.service.ts b/src/server/services/monitoring.service.ts index 812b76c..675dcb3 100644 --- a/src/server/services/monitoring.service.ts +++ b/src/server/services/monitoring.service.ts @@ -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 { 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> { const pvcFromApp = await pvcService.getAllPvcForApp(projectId, appId); const pvcUsageData: Array<{ pvcName: string, usedBytes: number }> = []; diff --git a/src/shared/model/app-monitoring-usage.model.ts b/src/shared/model/app-monitoring-usage.model.ts new file mode 100644 index 0000000..f14cb2e --- /dev/null +++ b/src/shared/model/app-monitoring-usage.model.ts @@ -0,0 +1,9 @@ +export interface AppMonitoringUsageModel { + projectId: string, + projectName: string, + appName: string, + appId: string, + cpuUsage: number, + cpuUsagePercent: number, + ramUsageBytes: number +}