diff --git a/src/views/virtualmachines/list/VirtualMachinesList.tsx b/src/views/virtualmachines/list/VirtualMachinesList.tsx index f1197df00..69497c11f 100644 --- a/src/views/virtualmachines/list/VirtualMachinesList.tsx +++ b/src/views/virtualmachines/list/VirtualMachinesList.tsx @@ -46,6 +46,7 @@ import VirtualMachineRow from './components/VirtualMachineRow/VirtualMachineRow' import VirtualMachinesCreateButton from './components/VirtualMachinesCreateButton/VirtualMachinesCreateButton'; import useSelectedFilters from './hooks/useSelectedFilters'; import useVirtualMachineColumns from './hooks/useVirtualMachineColumns'; +import useVMMetrics from './hooks/useVMMetrics'; import { deselectAll, selectAll, selectedVMs } from './selectedVMs'; import '@kubevirt-utils/styles/list-managment-group.scss'; @@ -63,6 +64,7 @@ const VirtualMachinesList: FC = ({ kind, namespace }) const isProxyPodAlive = useKubevirtDataPodHealth(); useSignals(); + useVMMetrics(); const query = useQuery(); diff --git a/src/views/virtualmachines/list/components/VirtualMachineRow/VirtualMachineRowLayout.tsx b/src/views/virtualmachines/list/components/VirtualMachineRow/VirtualMachineRowLayout.tsx index 75333e5c3..5da2a416a 100644 --- a/src/views/virtualmachines/list/components/VirtualMachineRow/VirtualMachineRowLayout.tsx +++ b/src/views/virtualmachines/list/components/VirtualMachineRow/VirtualMachineRowLayout.tsx @@ -17,6 +17,10 @@ import { setSelectedTreeItem, treeDataMap } from '@virtualmachines/tree/utils/ut import VirtualMachineStatus from '../VirtualMachineStatus/VirtualMachineStatus'; import { VMStatusConditionLabelList } from '../VMStatusConditionLabel'; +import CPUPercentage from './components/CPUPercentage'; +import MemoryPercentage from './components/MemoryPercentage'; +import NetworkUsage from './components/NetworkUsage'; + import './virtual-machine-row-layout.scss'; const VirtualMachineRowLayout: React.FC< @@ -32,6 +36,9 @@ const VirtualMachineRowLayout: React.FC< > = ({ activeColumnIDs, obj, rowData: { ips, isSingleNodeCluster, node, vmim } }) => { const selected = isVMSelected(obj); + const vmName = getName(obj); + const vmNamespace = getNamespace(obj); + const [actions] = useVirtualMachineActionsProvider(obj, vmim, isSingleNodeCluster); return ( <> @@ -45,11 +52,11 @@ const VirtualMachineRowLayout: React.FC< { - setSelectedTreeItem(treeDataMap.value[`${getNamespace(obj)}/${getName(obj)}`]); + setSelectedTreeItem(treeDataMap.value[`${vmNamespace}/${vmName}`]); }} groupVersionKind={VirtualMachineModelGroupVersionKind} - name={getName(obj)} - namespace={getNamespace(obj)} + name={vmName} + namespace={vmNamespace} /> - + @@ -82,6 +89,15 @@ const VirtualMachineRowLayout: React.FC< > {ips} + + + + + + + + + = ({ vmName, vmNamespace }) => { + const { cpuRequested, cpuUsage } = getVMMetrics(vmName, vmNamespace); + + if (isEmpty(cpuRequested) || isEmpty(cpuUsage)) return {NO_DATA_DASH}; + + const percentage = Math.round((cpuUsage * 10000) / cpuRequested) / 100; + return {percentage}%; +}; + +export default memo(CPUPercentage); diff --git a/src/views/virtualmachines/list/components/VirtualMachineRow/components/MemoryPercentage.tsx b/src/views/virtualmachines/list/components/VirtualMachineRow/components/MemoryPercentage.tsx new file mode 100644 index 000000000..0461e11e9 --- /dev/null +++ b/src/views/virtualmachines/list/components/VirtualMachineRow/components/MemoryPercentage.tsx @@ -0,0 +1,21 @@ +import React, { FC, memo } from 'react'; + +import { NO_DATA_DASH } from '@kubevirt-utils/resources/vm/utils/constants'; +import { isEmpty } from '@kubevirt-utils/utils/utils'; +import { getVMMetrics } from '@virtualmachines/list/metrics'; + +type MemoryPercentageProps = { + vmName: string; + vmNamespace: string; +}; + +const MemoryPercentage: FC = ({ vmName, vmNamespace }) => { + const { memoryRequested, memoryUsage } = getVMMetrics(vmName, vmNamespace); + + if (isEmpty(memoryRequested) || isEmpty(memoryUsage)) return {NO_DATA_DASH}; + + const percentage = Math.round((memoryUsage / memoryRequested) * 10000) / 100; + return {percentage}%; +}; + +export default memo(MemoryPercentage); diff --git a/src/views/virtualmachines/list/components/VirtualMachineRow/components/NetworkUsage.tsx b/src/views/virtualmachines/list/components/VirtualMachineRow/components/NetworkUsage.tsx new file mode 100644 index 000000000..94d681c99 --- /dev/null +++ b/src/views/virtualmachines/list/components/VirtualMachineRow/components/NetworkUsage.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import xbytes from 'xbytes'; + +import { NO_DATA_DASH } from '@kubevirt-utils/resources/vm/utils/constants'; +import { isEmpty } from '@kubevirt-utils/utils/utils'; +import { getVMMetrics } from '@virtualmachines/list/metrics'; + +type NetworkUsageProps = { + vmName: string; + vmNamespace: string; +}; + +const NetworkUsage: FC = ({ vmName, vmNamespace }) => { + const { networkUsage } = getVMMetrics(vmName, vmNamespace); + if (isEmpty(networkUsage)) return <>{NO_DATA_DASH}; + + const totalTransferred = xbytes(networkUsage || 0, { + fixed: 0, + iec: true, + }); + + return
{totalTransferred}ps
; +}; + +export default NetworkUsage; diff --git a/src/views/virtualmachines/list/hooks/constants.ts b/src/views/virtualmachines/list/hooks/constants.ts index 903482cc5..971260714 100644 --- a/src/views/virtualmachines/list/hooks/constants.ts +++ b/src/views/virtualmachines/list/hooks/constants.ts @@ -1,2 +1,25 @@ +import { ALL_NAMESPACES_SESSION_KEY } from '@kubevirt-utils/hooks/constants'; + export const TEXT_FILTER_NAME_ID = 'name'; export const TEXT_FILTER_LABELS_ID = 'labels'; + +export const VMListQueries = { + CPU_REQUESTED: 'CPU_REQUESTED', + CPU_USAGE: 'CPU_USAGE', + MEMORY_REQUESTED: 'MEMORY_REQUESTED', + MEMORY_USAGE: 'MEMORY_USAGE', + NETWORK_TOTAL_USAGE: 'NETWORK_TOTAL_USAGE', +}; + +export const getVMListQueries = (namespace: string) => { + const namespaceFilter = + namespace === ALL_NAMESPACES_SESSION_KEY ? '' : `namespace='${namespace}'`; + + return { + [VMListQueries.CPU_REQUESTED]: `kube_pod_resource_request{resource='cpu',${namespaceFilter}}`, + [VMListQueries.CPU_USAGE]: `rate(kubevirt_vmi_cpu_usage_seconds_total{${namespaceFilter}}[5m])`, + [VMListQueries.MEMORY_REQUESTED]: `kube_pod_resource_request{resource='memory',${namespaceFilter}}`, + [VMListQueries.MEMORY_USAGE]: `kubevirt_vmi_memory_used_bytes{${namespaceFilter}}`, + [VMListQueries.NETWORK_TOTAL_USAGE]: `rate(kubevirt_vmi_network_transmit_bytes_total{${namespaceFilter}}[5m]) + rate(kubevirt_vmi_network_receive_bytes_total{${namespaceFilter}}[5m])`, + }; +}; diff --git a/src/views/virtualmachines/list/hooks/useVMMetrics.ts b/src/views/virtualmachines/list/hooks/useVMMetrics.ts new file mode 100644 index 000000000..ced689208 --- /dev/null +++ b/src/views/virtualmachines/list/hooks/useVMMetrics.ts @@ -0,0 +1,132 @@ +import { useEffect, useMemo } from 'react'; + +import { IoK8sApiCoreV1Pod } from '@kubevirt-ui/kubevirt-api/kubernetes'; +import { ALL_NAMESPACES_SESSION_KEY } from '@kubevirt-utils/hooks/constants'; +import { modelToGroupVersionKind, PodModel } from '@kubevirt-utils/models'; +import { isEmpty } from '@kubevirt-utils/utils/utils'; +import { + PrometheusEndpoint, + useActiveNamespace, + useK8sWatchResource, + usePrometheusPoll, +} from '@openshift-console/dynamic-plugin-sdk'; + +import { + setVMCPURequested, + setVMCPUUsage, + setVMMemoryRequested, + setVMMemoryUsage, + setVMNetworkUsage, +} from '../metrics'; + +import { getVMListQueries, VMListQueries } from './constants'; +import { getVMNamesFromPodsNames } from './utils'; + +const useVMMetrics = () => { + const [activeNamespace] = useActiveNamespace(); + const allNamespace = activeNamespace === ALL_NAMESPACES_SESSION_KEY; + const currentTime = useMemo(() => Date.now(), []); + + const [pods] = useK8sWatchResource({ + groupVersionKind: modelToGroupVersionKind(PodModel), + isList: true, + namespace: allNamespace ? undefined : activeNamespace, + }); + + const launcherNameToVMName = useMemo(() => getVMNamesFromPodsNames(pods), [pods]); + + const queries = useMemo(() => getVMListQueries(activeNamespace), [activeNamespace]); + + const [memoryUsageResponse] = usePrometheusPoll({ + endpoint: PrometheusEndpoint?.QUERY, + endTime: currentTime, + namespace: allNamespace ? undefined : activeNamespace, + query: queries?.[VMListQueries.MEMORY_USAGE], + }); + + const [networkTotalResponse] = usePrometheusPoll({ + endpoint: PrometheusEndpoint?.QUERY, + endTime: currentTime, + namespace: allNamespace ? undefined : activeNamespace, + query: queries?.NETWORK_TOTAL_USAGE, + }); + + const [memoryRequestedResponse] = usePrometheusPoll({ + endpoint: PrometheusEndpoint?.QUERY, + endTime: currentTime, + namespace: allNamespace ? undefined : activeNamespace, + query: queries?.[VMListQueries.MEMORY_REQUESTED], + }); + + const [cpuUsageResponse] = usePrometheusPoll({ + endpoint: PrometheusEndpoint?.QUERY, + endTime: currentTime, + namespace: allNamespace ? undefined : activeNamespace, + query: queries?.[VMListQueries.CPU_USAGE], + }); + + const [cpuRequestedResponse] = usePrometheusPoll({ + endpoint: PrometheusEndpoint?.QUERY, + endTime: currentTime, + namespace: allNamespace ? undefined : activeNamespace, + query: queries?.[VMListQueries.CPU_REQUESTED], + }); + + useEffect(() => { + networkTotalResponse?.data?.result?.forEach((result) => { + const vmName = result?.metric?.name; + const vmNamespace = result?.metric?.namespace; + const memoryUsage = parseFloat(result?.value?.[1]); + + setVMNetworkUsage(vmName, vmNamespace, memoryUsage); + }); + }, [networkTotalResponse]); + + useEffect(() => { + memoryUsageResponse?.data?.result?.forEach((result) => { + const vmName = result?.metric?.name; + const vmNamespace = result?.metric?.namespace; + const memoryUsage = parseFloat(result?.value?.[1]); + + setVMMemoryUsage(vmName, vmNamespace, memoryUsage); + }); + }, [memoryUsageResponse]); + + useEffect(() => { + memoryRequestedResponse?.data?.result?.forEach((result) => { + const vmName = launcherNameToVMName?.[`${result?.metric?.namespace}-${result?.metric?.pod}`]; + + if (isEmpty(vmName)) return; + const vmNamespace = result?.metric?.namespace; + + const memoryRequested = parseFloat(result?.value?.[1]); + + setVMMemoryRequested(vmName, vmNamespace, memoryRequested); + }); + }, [memoryRequestedResponse, launcherNameToVMName]); + + useEffect(() => { + cpuUsageResponse?.data?.result?.forEach((result) => { + const vmName = result?.metric?.name; + const vmNamespace = result?.metric?.namespace; + const cpuUsage = parseFloat(result?.value?.[1]); + + setVMCPUUsage(vmName, vmNamespace, cpuUsage); + }); + }, [cpuUsageResponse]); + + useEffect(() => { + cpuRequestedResponse?.data?.result?.forEach((result) => { + const vmName = launcherNameToVMName?.[`${result?.metric?.namespace}-${result?.metric?.pod}`]; + + if (isEmpty(vmName)) return; + + const vmNamespace = result?.metric?.namespace; + const cpuRequested = parseFloat(result?.value?.[1]); + + setVMCPURequested(vmName, vmNamespace, cpuRequested); + }); + }, [cpuRequestedResponse, launcherNameToVMName]); +}; + +export default useVMMetrics; diff --git a/src/views/virtualmachines/list/hooks/useVirtualMachineColumns.ts b/src/views/virtualmachines/list/hooks/useVirtualMachineColumns.ts index 234d58f89..ae774c105 100644 --- a/src/views/virtualmachines/list/hooks/useVirtualMachineColumns.ts +++ b/src/views/virtualmachines/list/hooks/useVirtualMachineColumns.ts @@ -82,6 +82,21 @@ const useVirtualMachineColumns = ( props: { className: 'pf-m-width-10' }, title: t('IP address'), }, + { + additional: true, + id: 'memory-usage', + title: t('Memory'), + }, + { + additional: true, + id: 'cpu-usage', + title: t('CPU'), + }, + { + additional: true, + id: 'network-usage', + title: t('Network'), + }, { id: '', props: { className: 'dropdown-kebab-pf pf-v5-c-table__action' }, diff --git a/src/views/virtualmachines/list/hooks/utils.ts b/src/views/virtualmachines/list/hooks/utils.ts new file mode 100644 index 000000000..5d6d8ebcf --- /dev/null +++ b/src/views/virtualmachines/list/hooks/utils.ts @@ -0,0 +1,21 @@ +import VirtualMachineInstanceModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineInstanceModel'; +import { IoK8sApiCoreV1Pod } from '@kubevirt-ui/kubevirt-api/kubernetes'; +import { getName, getNamespace } from '@kubevirt-utils/resources/shared'; +import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; + +const getVMIOwner = (resource: K8sResourceCommon) => + resource?.metadata?.ownerReferences?.find( + (owner) => owner.kind === VirtualMachineInstanceModel.kind, + ); + +export const getVMNamesFromPodsNames = (pods: IoK8sApiCoreV1Pod[]) => { + return pods?.reduce((acc, pod) => { + const vmiOwner = getVMIOwner(pod); + + if (!vmiOwner) return acc; + + acc[`${getNamespace(pod)}-${getName(pod)}`] = vmiOwner.name; + + return acc; + }, {}); +}; diff --git a/src/views/virtualmachines/list/metrics.ts b/src/views/virtualmachines/list/metrics.ts new file mode 100644 index 000000000..dc19d03d4 --- /dev/null +++ b/src/views/virtualmachines/list/metrics.ts @@ -0,0 +1,55 @@ +import { signal } from '@preact/signals-core'; + +export type MetricsType = { + [key in string]: { + cpuRequested?: number; + cpuUsage?: number; + memoryRequested?: number; + memoryUsage?: number; + networkUsage?: number; + }; +}; + +export const vmMetrics = signal({}); + +export const getVMMetrics = (vmName: string, vmNamespace: string) => + vmMetrics.value?.[`${vmNamespace}-${vmName}`] || {}; + +export const setVMMemoryUsage = (vmName: string, vmNamespace: string, memoryUsage: number) => { + vmMetrics.value[`${vmNamespace}-${vmName}`] = { + ...getVMMetrics(vmName, vmNamespace), + memoryUsage, + }; +}; + +export const setVMNetworkUsage = (vmName: string, vmNamespace: string, networkUsage: number) => { + vmMetrics.value[`${vmNamespace}-${vmName}`] = { + ...getVMMetrics(vmName, vmNamespace), + networkUsage, + }; +}; + +export const setVMMemoryRequested = ( + vmName: string, + vmNamespace: string, + memoryRequested: number, +) => { + vmMetrics.value[`${vmNamespace}-${vmName}`] = { + ...getVMMetrics(vmName, vmNamespace), + memoryRequested, + }; +}; + +export const setVMCPUUsage = (vmName: string, vmNamespace: string, cpuUsage: number) => { + vmMetrics.value[`${vmNamespace}-${vmName}`] = { + ...getVMMetrics(vmName, vmNamespace), + cpuUsage, + }; +}; + +export const setVMCPURequested = (vmName: string, vmNamespace: string, cpuRequested: number) => { + vmMetrics.value[`${vmNamespace}-${vmName}`] = { + ...getVMMetrics(vmName, vmNamespace), + cpuRequested, + }; +};