fix(xo-server-usage-report): handle null and nested stats (#7092)

Introduced by 083483645e

Fixes Zammad#18120
Fixes Zammad#18266

- Always assume that data can be `null`
- Handle edge cases where all values are `null`
- Properly handle nested RRD collections: collections have different depths (`memory`: 1, `cpus[0]`: 2, `pifs.rx[0]`: 3, ...). This PR replaces `getLastDays` which wouldn't handle those depths properly, with `getDeepLastValues` which is run on the whole stat object and doesn't assume the depth of the collections. It finds any Array at any depth and slices it to only keep the last N values.
This commit is contained in:
Pierre Donias 2023-10-18 22:50:08 +02:00 committed by GitHub
parent 2924f82754
commit 4b12a6d31d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 98 additions and 57 deletions

View File

@ -21,6 +21,8 @@
- [Home] Fix OS icons alignment (PR [#7090](https://github.com/vatesfr/xen-orchestra/pull/7090))
- [SR/Advanced] Fix the total number of VDIs to coalesce by taking into account common chains [#7016](https://github.com/vatesfr/xen-orchestra/issues/7016) (PR [#7098](https://github.com/vatesfr/xen-orchestra/pull/7098))
- Don't require to sign in again in XO after losing connection to XO Server (e.g. when restarting or upgrading XO) (PR [#7103](https://github.com/vatesfr/xen-orchestra/pull/7103))
- [Usage report] Fix "Converting circular structure to JSON" error (PR [#7096](https://github.com/vatesfr/xen-orchestra/pull/7096))
- [Usage report] Fix "Cannot convert undefined or null to object" error (PR [#7092](https://github.com/vatesfr/xen-orchestra/pull/7092))
### Packages to release

View File

@ -12,9 +12,9 @@ import {
filter,
find,
forEach,
get,
isFinite,
map,
mapValues,
orderBy,
round,
values,
@ -204,6 +204,11 @@ function computeMean(values) {
}
})
// No values to work with, return null
if (n === 0) {
return null
}
return sum / n
}
@ -226,7 +231,7 @@ function getTop(objects, options) {
object => {
const value = object[opt]
return isNaN(value) ? -Infinity : value
return isNaN(value) || value === null ? -Infinity : value
},
'desc'
).slice(0, 3),
@ -244,7 +249,9 @@ function computePercentage(curr, prev, options) {
return zipObject(
options,
map(options, opt =>
prev[opt] === 0 || prev[opt] === null ? 'NONE' : `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
prev[opt] === 0 || prev[opt] === null || curr[opt] === null
? 'NONE'
: `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
)
)
}
@ -257,7 +264,15 @@ function getDiff(oldElements, newElements) {
}
function getMemoryUsedMetric({ memory, memoryFree = memory }) {
return map(memory, (value, key) => value - memoryFree[key])
return map(memory, (value, key) => {
const tMemory = value
const tMemoryFree = memoryFree[key]
if (tMemory == null || tMemoryFree == null) {
return null
}
return tMemory - tMemoryFree
})
}
const METRICS_MEAN = {
@ -274,27 +289,34 @@ const DAYS_TO_KEEP = {
weekly: 7,
monthly: 30,
}
function getLastDays(data, periodicity) {
const daysToKeep = DAYS_TO_KEEP[periodicity]
const expectedData = {}
for (const [key, value] of Object.entries(data)) {
if (Array.isArray(value)) {
// slice only applies to array
expectedData[key] = value.slice(-daysToKeep)
} else {
expectedData[key] = value
function getDeepLastValues(data, nValues) {
if (data == null) {
return {}
}
if (Array.isArray(data)) {
return data.slice(-nValues)
}
return expectedData
if (typeof data !== 'object') {
throw new Error('data must be an object or an array')
}
return mapValues(data, value => getDeepLastValues(value, nValues))
}
// ===================================================================
async function getVmsStats({ runningVms, periodicity, xo }) {
const lastNValues = DAYS_TO_KEEP[periodicity]
return orderBy(
await Promise.all(
map(runningVms, async vm => {
const { stats } = await xo.getXapiVmStats(vm, GRANULARITY).catch(error => {
const stats = getDeepLastValues(
(
await xo.getXapiVmStats(vm, GRANULARITY).catch(error => {
log.warn('Error on fetching VM stats', {
error,
vmId: vm.id,
@ -303,22 +325,25 @@ async function getVmsStats({ runningVms, periodicity, xo }) {
stats: {},
}
})
).stats,
lastNValues
)
const iopsRead = METRICS_MEAN.iops(getLastDays(get(stats.iops, 'r'), periodicity))
const iopsWrite = METRICS_MEAN.iops(getLastDays(get(stats.iops, 'w'), periodicity))
const iopsRead = METRICS_MEAN.iops(stats.iops?.r)
const iopsWrite = METRICS_MEAN.iops(stats.iops?.w)
return {
uuid: vm.uuid,
name: vm.name_label,
addresses: Object.values(vm.addresses),
cpu: METRICS_MEAN.cpu(getLastDays(stats.cpus, periodicity)),
ram: METRICS_MEAN.ram(getLastDays(getMemoryUsedMetric(stats), periodicity)),
diskRead: METRICS_MEAN.disk(getLastDays(get(stats.xvds, 'r'), periodicity)),
diskWrite: METRICS_MEAN.disk(getLastDays(get(stats.xvds, 'w'), periodicity)),
cpu: METRICS_MEAN.cpu(stats.cpus),
ram: METRICS_MEAN.ram(getMemoryUsedMetric(stats)),
diskRead: METRICS_MEAN.disk(stats.xvds?.r),
diskWrite: METRICS_MEAN.disk(stats.xvds?.w),
iopsRead,
iopsWrite,
iopsTotal: iopsRead + iopsWrite,
netReception: METRICS_MEAN.net(getLastDays(get(stats.vifs, 'rx'), periodicity)),
netTransmission: METRICS_MEAN.net(getLastDays(get(stats.vifs, 'tx'), periodicity)),
netReception: METRICS_MEAN.net(stats.vifs?.rx),
netTransmission: METRICS_MEAN.net(stats.vifs?.tx),
}
})
),
@ -328,10 +353,14 @@ async function getVmsStats({ runningVms, periodicity, xo }) {
}
async function getHostsStats({ runningHosts, periodicity, xo }) {
const lastNValues = DAYS_TO_KEEP[periodicity]
return orderBy(
await Promise.all(
map(runningHosts, async host => {
const { stats } = await xo.getXapiHostStats(host, GRANULARITY).catch(error => {
const stats = getDeepLastValues(
(
await xo.getXapiHostStats(host, GRANULARITY).catch(error => {
log.warn('Error on fetching host stats', {
error,
hostId: host.id,
@ -340,15 +369,18 @@ async function getHostsStats({ runningHosts, periodicity, xo }) {
stats: {},
}
})
).stats,
lastNValues
)
return {
uuid: host.uuid,
name: host.name_label,
cpu: METRICS_MEAN.cpu(getLastDays(stats.cpus, periodicity)),
ram: METRICS_MEAN.ram(getLastDays(getMemoryUsedMetric(stats), periodicity)),
load: METRICS_MEAN.load(getLastDays(stats.load, periodicity)),
netReception: METRICS_MEAN.net(getLastDays(get(stats.pifs, 'rx'), periodicity)),
netTransmission: METRICS_MEAN.net(getLastDays(get(stats.pifs, 'tx'), periodicity)),
cpu: METRICS_MEAN.cpu(stats.cpus),
ram: METRICS_MEAN.ram(getMemoryUsedMetric(stats)),
load: METRICS_MEAN.load(stats.load),
netReception: METRICS_MEAN.net(stats.pifs?.rx),
netTransmission: METRICS_MEAN.net(stats.pifs?.tx),
}
})
),
@ -358,6 +390,8 @@ async function getHostsStats({ runningHosts, periodicity, xo }) {
}
async function getSrsStats({ periodicity, xo, xoObjects }) {
const lastNValues = DAYS_TO_KEEP[periodicity]
return orderBy(
await asyncMapSettled(
filter(xoObjects, obj => obj.type === 'SR' && obj.size > 0 && obj.$PBDs.length > 0),
@ -371,7 +405,9 @@ async function getSrsStats({ periodicity, xo, xoObjects }) {
name += ` (${container.name_label})`
}
const { stats } = await xo.getXapiSrStats(sr.id, GRANULARITY).catch(error => {
const stats = getDeepLastValues(
(
await xo.getXapiSrStats(sr.id, GRANULARITY).catch(error => {
log.warn('Error on fetching SR stats', {
error,
srId: sr.id,
@ -380,9 +416,12 @@ async function getSrsStats({ periodicity, xo, xoObjects }) {
stats: {},
}
})
).stats,
lastNValues
)
const iopsRead = computeMean(getLastDays(get(stats.iops, 'r'), periodicity))
const iopsWrite = computeMean(getLastDays(get(stats.iops, 'w'), periodicity))
const iopsRead = computeMean(stats.iops?.r)
const iopsWrite = computeMean(stats.iops?.w)
return {
uuid: sr.uuid,