Skip to content

Commit

Permalink
feat: build stats for SRs
Browse files Browse the repository at this point in the history
Fixes #2644
  • Loading branch information
badrAZ committed Apr 9, 2018
1 parent 8746481 commit 5b49e12
Show file tree
Hide file tree
Showing 10 changed files with 505 additions and 7 deletions.
20 changes: 20 additions & 0 deletions packages/xo-server/src/api/sr.js
Expand Up @@ -838,3 +838,23 @@ getUnhealthyVdiChainsLength.params = {
getUnhealthyVdiChainsLength.resolve = {
sr: ['id', 'SR', 'operate'],
}

// -------------------------------------------------------------------

export function stats ({ sr, granularity }) {
return this.getXapiSrStats(sr._xapiId, granularity)
}

stats.description = 'returns statistic of the sr'

stats.params = {
id: { type: 'string' },
granularity: {
type: 'string',
optional: true,
},
}

stats.resolve = {
sr: ['id', 'SR', 'view'],
}
98 changes: 97 additions & 1 deletion packages/xo-server/src/xapi-stats.js
@@ -1,7 +1,18 @@
import JSON5 from 'json5'
import limitConcurrency from 'limit-concurrency-decorator'
import { BaseError } from 'make-error'
import { endsWith, findKey, forEach, get, identity, map } from 'lodash'
import {
endsWith,
findKey,
forEach,
get,
identity,
map,
mapValues,
mean,
sum,
zipWith,
} from 'lodash'

import { parseDateTime } from './xapi'

Expand Down Expand Up @@ -62,6 +73,9 @@ const computeValues = (dataRow, legendIndex, transformValue = identity) =>
transformValue(convertNanToNull(values[legendIndex]))
)

const computSrStats = (stats, path, transformValues) =>
zipWith(...map(stats, path), (...values) => transformValues(values))

// It browse the object in depth and initialise it's properties
// The targerPath can be a string or an array containing the depth
// targetPath: [a, b, c] => a.b.c
Expand Down Expand Up @@ -141,6 +155,45 @@ const STATS = {
getPath: matches => ['pifs', 'tx', matches[1]],
},
},
iops: {
r: {
test: /^iops_read_(\w+)$/,
getPath: matches => ['iops', 'r', matches[1]],
},
w: {
test: /^iops_write_(\w+)$/,
getPath: matches => ['iops', 'w', matches[1]],
},
},
ioThroughput: {
r: {
test: /^io_throughput_read_(\w+)$/,
getPath: matches => ['ioThroughput', 'r', matches[1]],
transformValue: value => value * 2 ** 20,
},
w: {
test: /^io_throughput_write_(\w+)$/,
getPath: matches => ['ioThroughput', 'w', matches[1]],
transformValue: value => value * 2 ** 20,
},
},
latency: {
r: {
test: /^read_latency_(\w+)$/,
getPath: matches => ['latency', 'r', matches[1]],
transformValue: value => value / 1e3,
},
w: {
test: /^write_latency_(\w+)$/,
getPath: matches => ['latency', 'w', matches[1]],
transformValue: value => value / 1e3,
},
},
iowait: {
test: /^iowait_(\w+)$/,
getPath: matches => ['iowait', matches[1]],
transformValue: value => value * 1e2,
},
},
vm: {
memoryFree: {
Expand Down Expand Up @@ -361,4 +414,47 @@ export default class XapiStats {
granularity,
})
}

async getSrStats (xapi, srId, granularity) {
const sr = xapi.getObject(srId)

const hostsStats = {}
await Promise.all(
map(map(sr.$PBDs, 'host'), hostId =>
this.getHostStats(xapi, hostId, granularity).then(stats => {
hostsStats[xapi.getObject(hostId).name_label] = stats
})
)
)

const srShortUUID = sr.uuid.slice(0, 8)
return {
interval: hostsStats[Object.keys(hostsStats)[0]].interval,
endTimestamp: Math.max(...map(hostsStats, 'endTimestamp')),
localTimestamp: Math.min(...map(hostsStats, 'localTimestamp')),
stats: {
iops: {
r: computSrStats(hostsStats, `stats.iops.r[${srShortUUID}]`, sum),
w: computSrStats(hostsStats, `stats.iops.w[${srShortUUID}]`, sum),
},
ioThroughput: {
r: computSrStats(
hostsStats,
`stats.ioThroughput.r[${srShortUUID}]`,
sum
),
w: computSrStats(
hostsStats,
`stats.ioThroughput.w[${srShortUUID}]`,
sum
),
},
latency: {
r: computSrStats(hostsStats, `stats.latency.r[${srShortUUID}]`, mean),
w: computSrStats(hostsStats, `stats.latency.w[${srShortUUID}]`, mean),
},
iowait: mapValues(hostsStats, `stats.iowait[${srShortUUID}]`),
},
}
}
}
4 changes: 4 additions & 0 deletions packages/xo-server/src/xo-mixins/xen-servers.js
Expand Up @@ -400,6 +400,10 @@ export default class {
return this._stats.getHostStats(this.getXapi(hostId), hostId, granularity)
}

getXapiSrStats (srId, granularity) {
return this._stats.getSrStats(this.getXapi(srId), srId, granularity)
}

async mergeXenPools (sourceId, targetId, force = false) {
const sourceXapi = this.getXapi(sourceId)
const { _auth: { user, password }, _url: { hostname } } = this.getXapi(
Expand Down
8 changes: 8 additions & 0 deletions packages/xo-web/src/common/intl/messages.js
Expand Up @@ -543,6 +543,14 @@ const messages = {
srUnhealthyVdiDepth: 'Depth',
srUnhealthyVdiTitle: 'VDI to coalesce ({total, number})',

// ----- SR stats tab -----

srNoStats: 'No stats',
statsIops: 'IOPS',
statsIoThroughput: 'IO throughput',
statsLatency: 'Latency',
statsIowait: 'IOwait',

// ----- SR actions -----
srRescan: 'Rescan all disks',
srReconnectAll: 'Connect to all hosts',
Expand Down
13 changes: 13 additions & 0 deletions packages/xo-web/src/common/utils.js
Expand Up @@ -205,6 +205,19 @@ export const formatSizeRaw = bytes =>
export const formatSpeed = (bytes, milliseconds) =>
humanFormat(bytes * 1e3 / milliseconds, { scale: 'binary', unit: 'B/s' })

const timeScale = new humanFormat.Scale({
ns: 1e-6,
µs: 1e-3,
ms: 1,
s: 1e3,
min: 60 * 1e3,
h: 3600 * 1e3,
d: 86400 * 1e3,
y: 2592000 * 1e3,
})
export const formatTime = milliseconds =>
humanFormat(milliseconds, { scale: timeScale, decimals: 0 })

export const parseSize = size => {
let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
if (bytes.unit && bytes.unit !== 'B') {
Expand Down
178 changes: 176 additions & 2 deletions packages/xo-web/src/common/xo-line-chart/index.js
Expand Up @@ -4,11 +4,16 @@ import ChartistTooltip from 'chartist-plugin-tooltip'
import React from 'react'
import { injectIntl } from 'react-intl'
import { messages } from 'intl'
import { find, flatten, floor, map, max, size, sum, values } from 'lodash'
import { find, flatten, floor, get, map, max, size, sum, values } from 'lodash'

import propTypes from '../prop-types-decorator'
import { computeArraysSum } from '../xo-stats'
import { formatSize, getMemoryUsedMetric } from '../utils'
import {
formatSize,
formatSpeed,
formatTime,
getMemoryUsedMetric,
} from '../utils'

import styles from './index.css'

Expand Down Expand Up @@ -555,3 +560,172 @@ export const PoolLoadLineChart = injectIntl(
)
})
)

const buildSrSeries = ({ stats, label, addSumSeries }) => {
const series = map(stats, (data, key) => ({
name: `${label} (${key})`,
data,
}))

if (addSumSeries) {
series.push({
name: `All ${label}`,
data: computeArraysSum(values(stats)),
className: styles.dashedLine,
})
}

return series
}

export const IopsLineChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.array.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const { endTimestamp, interval, stats: { iops } } = data

const { length } = get(iops, 'r')

if (length === 0) {
return templateError
}

return (
<ChartistGraph
type='Line'
data={{
series: buildSrSeries({ stats: iops, label: 'Iops', addSumSeries }),
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp,
interval,
valueTransform: value => `${value.toPrecision(3)} /s`,
}),
...options,
}}
/>
)
})
)

export const IoThroughputChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.array.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const { endTimestamp, interval, stats: { ioThroughput } } = data

const { length } = get(ioThroughput, 'r') || []

if (length === 0) {
return templateError
}

return (
<ChartistGraph
type='Line'
data={{
series: buildSrSeries({
stats: ioThroughput,
label: 'IO throughput',
addSumSeries,
}),
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp,
interval,
valueTransform: value => formatSpeed(value, 1e3),
}),
...options,
}}
/>
)
})
)

export const LatencyChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.array.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const { endTimestamp, interval, stats: { latency } } = data

const { length } = get(latency, 'r') || []

if (length === 0) {
return templateError
}

return (
<ChartistGraph
type='Line'
data={{
series: buildSrSeries({
stats: latency,
label: 'Latency',
addSumSeries,
}),
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp,
interval,
valueTransform: value => formatTime(value),
}),
...options,
}}
/>
)
})
)

export const IowaitChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.array.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const { endTimestamp, interval, stats: { iowait } } = data

const { length } = iowait[Object.keys(iowait)[0]] || []

if (length === 0) {
return templateError
}

return (
<ChartistGraph
type='Line'
data={{
series: buildSrSeries({
stats: iowait,
label: 'IOwait',
addSumSeries,
}),
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp,
interval,
valueTransform: value => `${value.toPrecision(2)}%`,
}),
...options,
}}
/>
)
})
)
3 changes: 3 additions & 0 deletions packages/xo-web/src/common/xo/index.js
Expand Up @@ -1495,6 +1495,9 @@ export const deleteSr = sr =>
),
}).then(() => _call('sr.destroy', { id: resolveId(sr) }), noop)

export const fetchSrStats = (sr, granularity) =>
_call('sr.stats', { id: resolveId(sr), granularity })

export const forgetSr = sr =>
confirm({
title: _('srForgetModalTitle'),
Expand Down

0 comments on commit 5b49e12

Please sign in to comment.