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:
parent
2924f82754
commit
4b12a6d31d
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user