feat(xo-web): stats for SRs (#2847)
This commit is contained in:
parent
0c027247ec
commit
a8ba4a1a8e
@ -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'],
|
||||
}
|
||||
|
@ -1,7 +1,19 @@
|
||||
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,
|
||||
uniq,
|
||||
zipWith,
|
||||
} from 'lodash'
|
||||
|
||||
import { parseDateTime } from './xapi'
|
||||
|
||||
@ -62,6 +74,9 @@ const computeValues = (dataRow, legendIndex, transformValue = identity) =>
|
||||
transformValue(convertNanToNull(values[legendIndex]))
|
||||
)
|
||||
|
||||
const combineStats = (stats, path, combineValues) =>
|
||||
zipWith(...map(stats, path), (...values) => combineValues(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
|
||||
@ -141,6 +156,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: {
|
||||
@ -361,4 +415,47 @@ export default class XapiStats {
|
||||
granularity,
|
||||
})
|
||||
}
|
||||
|
||||
async getSrStats (xapi, srId, granularity) {
|
||||
const sr = xapi.getObject(srId)
|
||||
|
||||
const hostsStats = {}
|
||||
await Promise.all(
|
||||
map(uniq(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: combineStats(hostsStats, `stats.iops.r[${srShortUUID}]`, sum),
|
||||
w: combineStats(hostsStats, `stats.iops.w[${srShortUUID}]`, sum),
|
||||
},
|
||||
ioThroughput: {
|
||||
r: combineStats(
|
||||
hostsStats,
|
||||
`stats.ioThroughput.r[${srShortUUID}]`,
|
||||
sum
|
||||
),
|
||||
w: combineStats(
|
||||
hostsStats,
|
||||
`stats.ioThroughput.w[${srShortUUID}]`,
|
||||
sum
|
||||
),
|
||||
},
|
||||
latency: {
|
||||
r: combineStats(hostsStats, `stats.latency.r[${srShortUUID}]`, mean),
|
||||
w: combineStats(hostsStats, `stats.latency.w[${srShortUUID}]`, mean),
|
||||
},
|
||||
iowait: mapValues(hostsStats, `stats.iowait[${srShortUUID}]`),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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',
|
||||
|
@ -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') {
|
||||
|
@ -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'
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)
|
||||
|
@ -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'),
|
||||
|
@ -237,6 +237,18 @@
|
||||
@extend .fa;
|
||||
@extend .fa-database;
|
||||
}
|
||||
&-iops {
|
||||
@extend .fa;
|
||||
@extend .fa-cogs;
|
||||
}
|
||||
&-latency {
|
||||
@extend .fa;
|
||||
@extend .fa-clock-o;
|
||||
}
|
||||
&-iowait {
|
||||
@extend .fa;
|
||||
@extend .fa-pause;
|
||||
}
|
||||
&-delete {
|
||||
@extend .fa;
|
||||
@extend .fa-trash;
|
||||
|
@ -20,20 +20,22 @@ import {
|
||||
} from 'selectors'
|
||||
|
||||
import TabAdvanced from './tab-advanced'
|
||||
import TabGeneral from './tab-general'
|
||||
import TabLogs from './tab-logs'
|
||||
import TabHosts from './tab-host'
|
||||
import TabDisks from './tab-disks'
|
||||
import TabGeneral from './tab-general'
|
||||
import TabHosts from './tab-host'
|
||||
import TabLogs from './tab-logs'
|
||||
import TabStats from './tab-stats'
|
||||
import TabXosan from './tab-xosan'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@routes('general', {
|
||||
advanced: TabAdvanced,
|
||||
general: TabGeneral,
|
||||
logs: TabLogs,
|
||||
hosts: TabHosts,
|
||||
disks: TabDisks,
|
||||
general: TabGeneral,
|
||||
hosts: TabHosts,
|
||||
logs: TabLogs,
|
||||
stats: TabStats,
|
||||
xosan: TabXosan,
|
||||
})
|
||||
@connectStore(() => {
|
||||
@ -143,6 +145,7 @@ export default class Sr extends Component {
|
||||
<NavLink to={`/srs/${sr.id}/general`}>
|
||||
{_('generalTabName')}
|
||||
</NavLink>
|
||||
<NavLink to={`/srs/${sr.id}/stats`}>{_('statsTabName')}</NavLink>
|
||||
<NavLink to={`/srs/${sr.id}/disks`}>
|
||||
{_('disksTabName', { disks: sr.VDIs.length })}
|
||||
</NavLink>
|
||||
|
165
packages/xo-web/src/xo-app/sr/tab-stats.js
Normal file
165
packages/xo-web/src/xo-app/sr/tab-stats.js
Normal file
@ -0,0 +1,165 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { fetchSrStats } from 'xo'
|
||||
import { get } from 'lodash'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
IopsLineChart,
|
||||
IoThroughputChart,
|
||||
IowaitChart,
|
||||
LatencyChart,
|
||||
} from 'xo-line-chart'
|
||||
|
||||
export default class SrStats extends Component {
|
||||
state = {
|
||||
granularity: 'seconds',
|
||||
}
|
||||
|
||||
_loop (sr = get(this.props, 'sr')) {
|
||||
if (sr === undefined) {
|
||||
this._loop()
|
||||
}
|
||||
|
||||
if (this.cancel !== undefined) {
|
||||
this.cancel()
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
this.cancel = () => {
|
||||
cancelled = true
|
||||
}
|
||||
|
||||
fetchSrStats(sr, this.state.granularity).then(data => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
this.cancel = undefined
|
||||
|
||||
clearTimeout(this.timeout)
|
||||
this.setState(
|
||||
{
|
||||
data,
|
||||
selectStatsLoading: false,
|
||||
},
|
||||
() => {
|
||||
this.timeout = setTimeout(this._loop, data.interval * 1e3)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
_loop = ::this._loop
|
||||
|
||||
componentWillMount () {
|
||||
this._loop()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
_onGranularityChange = ({ target: { value: granularity } }) => {
|
||||
clearTimeout(this.timeout)
|
||||
this.setState(
|
||||
{
|
||||
granularity,
|
||||
selectStatsLoading: true,
|
||||
},
|
||||
this._loop
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
data,
|
||||
granularity,
|
||||
selectStatsLoading,
|
||||
useCombinedValues,
|
||||
} = this.state
|
||||
|
||||
return data === undefined ? (
|
||||
<span>{_('srNoStats')}</span>
|
||||
) : (
|
||||
<Upgrade place='srStats' available={3}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={5}>
|
||||
<div className='form-group'>
|
||||
<Tooltip content={_('useStackedValuesOnStats')}>
|
||||
<Toggle
|
||||
value={useCombinedValues}
|
||||
onChange={this.linkState('useCombinedValues')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
{selectStatsLoading && (
|
||||
<div className='text-xs-right'>
|
||||
<Icon icon='loading' size={2} />
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._onGranularityChange}
|
||||
defaultValue={granularity}
|
||||
>
|
||||
{_('statLastTenMinutes', message => (
|
||||
<option value='seconds'>{message}</option>
|
||||
))}
|
||||
{_('statLastTwoHours', message => (
|
||||
<option value='minutes'>{message}</option>
|
||||
))}
|
||||
{_('statLastWeek', message => (
|
||||
<option value='hours'>{message}</option>
|
||||
))}
|
||||
{_('statLastYear', message => (
|
||||
<option value='days'>{message}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='iops' size={1} /> {_('statsIops')}
|
||||
</h5>
|
||||
<IopsLineChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='disk' size={1} /> {_('statsIoThroughput')}
|
||||
</h5>
|
||||
<IoThroughputChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='latency' size={1} /> {_('statsLatency')}
|
||||
</h5>
|
||||
<LatencyChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='iowait' size={1} /> {_('statsIowait')}
|
||||
</h5>
|
||||
<IowaitChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Upgrade>
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user