feat(lite/pool/dashboard): top 5 CPU usage (#6370)

This commit is contained in:
Mathieu 2022-10-18 11:02:07 +02:00 committed by Julien Fontanet
parent ffc3249b33
commit 9963568368
12 changed files with 302 additions and 38 deletions

View File

@ -1,16 +1,18 @@
<template> <template>
<div class="header"> <div v-if="data.length !== 0">
<slot name="header" /> <div class="header">
</div> <slot name="header" />
<ProgressBar </div>
v-for="(item, index) in computedData.sortedArray" <ProgressBar
:key="index" v-for="item in computedData.sortedArray"
:value="item.value" :key="item.id"
:label="item.label" :value="item.value"
:badge-label="item.badgeLabel" :label="item.label"
/> :badge-label="item.badgeLabel"
<div class="footer"> />
<slot name="footer" :total-percent="computedData.totalPercentUsage" /> <div class="footer">
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
</div>
</div> </div>
</template> </template>
@ -19,6 +21,7 @@ import { computed } from "vue";
import ProgressBar from "@/components/ProgressBar.vue"; import ProgressBar from "@/components/ProgressBar.vue";
interface Data { interface Data {
id: string;
value: number; value: number;
label?: string; label?: string;
badgeLabel?: string; badgeLabel?: string;
@ -27,7 +30,7 @@ interface Data {
interface Props { interface Props {
data: Array<Data>; data: Array<Data>;
title?: string; nItems?: number;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
@ -45,7 +48,8 @@ const computedData = computed(() => {
value, value,
}; };
}) })
.sort((item, nextItem) => nextItem.value - item.value), .sort((item, nextItem) => nextItem.value - item.value)
.slice(0, props.nItems ?? _data.length),
totalPercentUsage, totalPercentUsage,
}; };
}); });

View File

@ -0,0 +1,13 @@
<template>
<UiCard>
<UiTitle type="h4">{{ $t("cpu-usage") }}</UiTitle>
<HostsCpuUsage />
<VmsCpuUsage />
</UiCard>
</template>
<script lang="ts" setup>
import HostsCpuUsage from "@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue";
import VmsCpuUsage from "@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
</script>

View File

@ -0,0 +1,46 @@
<template>
<UsageBar :data="data" :n-items="5">
<template #header>
<span>{{ $t("hosts") }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import { getAvgCpuUsage } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
const stats: ComputedRef<
{
id: string;
name: string;
stats?: HostStats;
}[]
> = inject<any>("hostStats", []);
const data = computed<{ id: string; label: string; value: number }[]>(() => {
const result: { id: string; label: string; value: number }[] = [];
stats.value.forEach((stat) => {
if (stat.stats === undefined) {
return;
}
const avgCpuUsage = getAvgCpuUsage(stat.stats.cpus);
if (avgCpuUsage === undefined) {
return;
}
result.push({
id: stat.id,
label: stat.name,
value: avgCpuUsage,
});
});
return result;
});
</script>

View File

@ -0,0 +1,46 @@
<template>
<UsageBar :data="data" :n-items="5">
<template #header>
<span>{{ $t("vms") }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import { getAvgCpuUsage } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
const stats: ComputedRef<
{
id: string;
name: string;
stats?: VmStats;
}[]
> = inject<any>("vmStats", []);
const data = computed<{ id: string; label: string; value: number }[]>(() => {
const result: { id: string; label: string; value: number }[] = [];
stats.value.forEach((stat) => {
if (!stat.stats) {
return;
}
const avgCpuUsage = getAvgCpuUsage(stat.stats.cpus);
if (avgCpuUsage === undefined) {
return;
}
result.push({
id: stat.id,
label: stat.name,
value: avgCpuUsage,
});
});
return result;
});
</script>

View File

@ -1,23 +1,25 @@
# useFetchStats composable # useFetchStats composable
```vue ```vue
<div>
<p v-for="(stat, index) in stats" :key="index">
{{ stat.name }}
</p>
</div>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from "vue";
import useFetchStats from "@/composables/fetch-stats-composable"; import useFetchStats from "@/composables/fetch-stats-composable";
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats"; import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
const vmId = "1d381a66-d1cb-bb7e-50a1-feeab58b293d"; const vmStore = useVmStore();
const hostId = "0aea61f4-c9d1-4060-94e8-4eb2024d082c";
const { stats: vmStats } = useFetchStats<VmStats>( const { register, unregister, stats } = useFetchStats<XenApiVm, VmStats>(
"vm", "vm",
vmId,
GRANULARITY.Seconds GRANULARITY.Seconds
); );
const { stats: hostStats } = useFetchStats<HostStats>( onMounted(() => {
"host", vmStore.allRecords.forEach(register);
hostId, });
GRANULARITY.Seconds
);
</script> </script>
``` ```

View File

@ -1,6 +1,7 @@
import { type Ref, ref } from "vue"; import { computed, onUnmounted, ref } from "vue";
import { promiseTimeout, useIntervalFn } from "@vueuse/core"; import { type Pausable, promiseTimeout, useTimeoutPoll } from "@vueuse/core";
import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats"; import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store"; import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store"; import { useVmStore } from "@/stores/vm.store";
@ -9,19 +10,59 @@ const STORES_BY_OBJECT_TYPE = {
vm: useVmStore, vm: useVmStore,
}; };
export default function useFetchStats<T>( export default function useFetchStats<T extends XenApiHost | XenApiVm, S>(
type: "host" | "vm", type: "host" | "vm",
id: string,
granularity: GRANULARITY granularity: GRANULARITY
) { ) {
const stats = ref(); const stats = ref<
const fetch = STORES_BY_OBJECT_TYPE[type]().getStats; Map<string, { id: string; name: string; stats?: S; pausable: Pausable }>
>(new Map());
const fetchStats = async () => { const register = (object: T) => {
stats.value = await fetch(id, granularity); if (stats.value.has(object.uuid)) {
await promiseTimeout(stats.value.interval * 1000); stats.value.get(object.uuid)!.pausable.resume();
return;
}
const pausable = useTimeoutPoll(
async () => {
if (!stats.value.has(object.uuid)) {
return;
}
const newStats = (await STORES_BY_OBJECT_TYPE[type]().getStats(
object.uuid,
granularity
)) as XapiStatsResponse<S>;
stats.value.get(object.uuid)!.stats = newStats.stats;
await promiseTimeout(newStats.interval * 1000);
},
0,
{ immediate: true }
);
stats.value.set(object.uuid, {
id: object.uuid,
name: object.name_label,
stats: undefined,
pausable,
});
}; };
useIntervalFn(fetchStats);
return { stats } as { stats: Ref<XapiStatsResponse<T>> | undefined }; const unregister = (object: T) => {
stats.value.get(object.uuid)?.pausable.pause();
stats.value.delete(object.uuid);
};
onUnmounted(() => {
stats.value.forEach((stat) => stat.pausable.pause());
});
return {
register,
unregister,
stats: computed(() => Array.from(stats.value.values())),
};
} }

View File

@ -1,9 +1,13 @@
import { utcParse } from "d3-time-format"; import { utcParse } from "d3-time-format";
import humanFormat from "human-format"; import humanFormat from "human-format";
import { round } from "lodash-es"; import { round } from "lodash-es";
import { find, forEach, isEqual, size, sum } from "lodash-es";
import { type ComputedGetter, type Ref, computed, ref, watchEffect } from "vue";
import type { Filter } from "@/types/filter"; import type { Filter } from "@/types/filter";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons"; import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import { faFont, faHashtag, faList } from "@fortawesome/free-solid-svg-icons"; import { faFont, faHashtag, faList } from "@fortawesome/free-solid-svg-icons";
import type { RawXenApiRecord, XenApiHost, XenApiRecord } from "@/libs/xen-api";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
export function sortRecordsByNameLabel( export function sortRecordsByNameLabel(
record1: { name_label: string }, record1: { name_label: string },
@ -67,3 +71,55 @@ export const hasEllipsis = (target: Element | undefined | null) =>
export function percent(currentValue: number, maxValue: number, precision = 2) { export function percent(currentValue: number, maxValue: number, precision = 2) {
return round((currentValue / maxValue) * 100, precision); return round((currentValue / maxValue) * 100, precision);
} }
export function getAvgCpuUsage(cpus?: object | any[], { nSequence = 4 } = {}) {
const statsLength = getStatsLength(cpus);
if (statsLength === undefined) {
return;
}
const _nSequence = statsLength < nSequence ? statsLength : nSequence;
let totalCpusUsage = 0;
forEach(cpus, (cpuState: number[]) => {
totalCpusUsage += sum(cpuState.slice(cpuState.length - _nSequence));
});
const stackedValue = totalCpusUsage / _nSequence;
return stackedValue / size(cpus);
}
// stats can be null.
// Return the size of the first non-null object.
export function getStatsLength(stats?: object | any[]) {
if (stats === undefined) {
return undefined;
}
return size(find(stats, (stat) => stat != null));
}
export function deepComputed<T>(getter: ComputedGetter<T>) {
const value = computed(getter);
const cache = ref<T>(value.value) as Ref<T>;
watchEffect(() => {
if (!isEqual(cache.value, value.value)) {
cache.value = value.value;
}
});
return cache;
}
export function isHostRunning(host: XenApiHost) {
const store = useHostMetricsStore();
try {
return store.getRecord(host.metrics).live;
} catch (_) {
return undefined;
}
}
export const buildXoObject = (
record: RawXenApiRecord<XenApiRecord>,
params: { opaqueRef: string }
) => ({
...record,
$ref: params.opaqueRef,
});

View File

@ -1,5 +1,5 @@
import { JSONRPCClient } from "json-rpc-2.0"; import { JSONRPCClient } from "json-rpc-2.0";
import { parseDateTime } from "@/libs/utils"; import { buildXoObject, parseDateTime } from "@/libs/utils";
export type RawObjectType = export type RawObjectType =
| "Bond" | "Bond"
@ -66,7 +66,7 @@ export interface XenApiRecord {
uuid: string; uuid: string;
} }
type RawXenApiRecord<T extends XenApiRecord> = Omit<T, "$ref">; export type RawXenApiRecord<T extends XenApiRecord> = Omit<T, "$ref">;
export interface XenApiPool extends XenApiRecord { export interface XenApiPool extends XenApiRecord {
name_label: string; name_label: string;
@ -232,7 +232,7 @@ export default class XenApi {
const entries = Object.entries(result).map<[string, T]>(([key, entry]) => [ const entries = Object.entries(result).map<[string, T]>(([key, entry]) => [
key, key,
{ $ref: key, ...entry } as T, buildXoObject(entry, { opaqueRef: key }) as T,
]); ]);
return new Map(entries); return new Map(entries);

View File

@ -10,6 +10,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"change-power-state": "Change power state", "change-power-state": "Change power state",
"copy": "Copy", "copy": "Copy",
"cpu-usage":"CPU usage",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"delete": "Delete", "delete": "Delete",
"descending": "descending", "descending": "descending",

View File

@ -10,6 +10,7 @@
"cancel": "Annuler", "cancel": "Annuler",
"change-power-state": "Changer l'état d'alimentation", "change-power-state": "Changer l'état d'alimentation",
"copy": "Copier", "copy": "Copier",
"cpu-usage":"Utilisation CPU",
"dashboard": "Tableau de bord", "dashboard": "Tableau de bord",
"delete": "Supprimer", "delete": "Supprimer",
"descending": "descendant", "descending": "descendant",

View File

@ -1,5 +1,6 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { reactive, shallowReactive } from "vue"; import { reactive, shallowReactive } from "vue";
import { buildXoObject } from "@/libs/utils";
import type { ObjectType, RawObjectType, XenApiRecord } from "@/libs/xen-api"; import type { ObjectType, RawObjectType, XenApiRecord } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store"; import { useXenApiStore } from "@/stores/xen-api.store";
@ -38,7 +39,7 @@ export const useRecordsStore = defineStore("records", () => {
opaqueRef: string, opaqueRef: string,
record: T record: T
) { ) {
recordsByOpaqueRef.set(opaqueRef, record); recordsByOpaqueRef.set(opaqueRef, buildXoObject(record, { opaqueRef }));
opaqueRefsByObjectType.get(objectType)?.add(opaqueRef); opaqueRefsByObjectType.get(objectType)?.add(opaqueRef);
uuidToOpaqueRefMapping.set(record.uuid, opaqueRef); uuidToOpaqueRefMapping.set(record.uuid, opaqueRef);
} }

View File

@ -2,12 +2,65 @@
<div class="pool-dashboard-view"> <div class="pool-dashboard-view">
<PoolDashboardStatus class="item" /> <PoolDashboardStatus class="item" />
<PoolDashboardStorageUsage class="item" /> <PoolDashboardStorageUsage class="item" />
<PoolDashboardCpuUsage class="item" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { differenceBy } from "lodash-es";
import { computed, onMounted, provide, readonly, watch } from "vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue"; import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue"; import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
import useFetchStats from "@/composables/fetch-stats.composable";
import { isHostRunning } from "@/libs/utils";
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
const hostStore = useHostStore();
const vmStore = useVmStore();
const {
register: hostRegister,
unregister: hostUnregister,
stats: hostStats,
} = useFetchStats<XenApiHost, HostStats>("host", GRANULARITY.Seconds);
const {
register: vmRegister,
unregister: vmUnregister,
stats: vmStats,
} = useFetchStats<XenApiVm, VmStats>("vm", GRANULARITY.Seconds);
const runningHosts = computed(() => hostStore.allRecords.filter(isHostRunning));
const runningVms = computed(() =>
vmStore.allRecords.filter((vm) => vm.power_state === "Running")
);
provide("hostStats", readonly(hostStats));
provide("vmStats", readonly(vmStats));
watch(runningHosts, (hosts, previousHosts) => {
// turned On
differenceBy(hosts, previousHosts ?? [], "uuid").forEach(hostRegister);
// turned Off
differenceBy(previousHosts, hosts, "uuid").forEach(hostUnregister);
});
watch(runningVms, (vms, previousVms) => {
// turned On
differenceBy(vms, previousVms ?? [], "uuid").forEach(vmRegister);
// turned Off
differenceBy(previousVms, vms, "uuid").forEach(vmUnregister);
});
onMounted(() => {
runningHosts.value.forEach(hostRegister);
runningVms.value.forEach(vmRegister);
});
</script> </script>
<style lang="postcss" scoped> <style lang="postcss" scoped>