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)) - [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)) - [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)) - 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 ### Packages to release

View File

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