feat(xo-web): stats for SRs (#2847)

This commit is contained in:
badrAZ 2018-04-16 14:40:00 +02:00 committed by Julien Fontanet
parent 0c027247ec
commit a8ba4a1a8e
10 changed files with 508 additions and 9 deletions

View File

@ -838,3 +838,23 @@ getUnhealthyVdiChainsLength.params = {
getUnhealthyVdiChainsLength.resolve = { getUnhealthyVdiChainsLength.resolve = {
sr: ['id', 'SR', 'operate'], 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'],
}

View File

@ -1,7 +1,19 @@
import JSON5 from 'json5' import JSON5 from 'json5'
import limitConcurrency from 'limit-concurrency-decorator' import limitConcurrency from 'limit-concurrency-decorator'
import { BaseError } from 'make-error' 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' import { parseDateTime } from './xapi'
@ -62,6 +74,9 @@ const computeValues = (dataRow, legendIndex, transformValue = identity) =>
transformValue(convertNanToNull(values[legendIndex])) 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 // It browse the object in depth and initialise it's properties
// The targerPath can be a string or an array containing the depth // The targerPath can be a string or an array containing the depth
// targetPath: [a, b, c] => a.b.c // targetPath: [a, b, c] => a.b.c
@ -141,6 +156,45 @@ const STATS = {
getPath: matches => ['pifs', 'tx', matches[1]], 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: { vm: {
memoryFree: { memoryFree: {
@ -361,4 +415,47 @@ export default class XapiStats {
granularity, 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}]`),
},
}
}
} }

View File

@ -400,6 +400,10 @@ export default class {
return this._stats.getHostStats(this.getXapi(hostId), hostId, granularity) 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) { async mergeXenPools (sourceId, targetId, force = false) {
const sourceXapi = this.getXapi(sourceId) const sourceXapi = this.getXapi(sourceId)
const { _auth: { user, password }, _url: { hostname } } = this.getXapi( const { _auth: { user, password }, _url: { hostname } } = this.getXapi(

View File

@ -543,6 +543,14 @@ const messages = {
srUnhealthyVdiDepth: 'Depth', srUnhealthyVdiDepth: 'Depth',
srUnhealthyVdiTitle: 'VDI to coalesce ({total, number})', srUnhealthyVdiTitle: 'VDI to coalesce ({total, number})',
// ----- SR stats tab -----
srNoStats: 'No stats',
statsIops: 'IOPS',
statsIoThroughput: 'IO throughput',
statsLatency: 'Latency',
statsIowait: 'IOwait',
// ----- SR actions ----- // ----- SR actions -----
srRescan: 'Rescan all disks', srRescan: 'Rescan all disks',
srReconnectAll: 'Connect to all hosts', srReconnectAll: 'Connect to all hosts',

View File

@ -205,6 +205,19 @@ export const formatSizeRaw = bytes =>
export const formatSpeed = (bytes, milliseconds) => export const formatSpeed = (bytes, milliseconds) =>
humanFormat(bytes * 1e3 / milliseconds, { scale: 'binary', unit: 'B/s' }) 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 => { export const parseSize = size => {
let bytes = humanFormat.parse.raw(size, { scale: 'binary' }) let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
if (bytes.unit && bytes.unit !== 'B') { if (bytes.unit && bytes.unit !== 'B') {

View File

@ -4,11 +4,16 @@ import ChartistTooltip from 'chartist-plugin-tooltip'
import React from 'react' import React from 'react'
import { injectIntl } from 'react-intl' import { injectIntl } from 'react-intl'
import { messages } from '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 propTypes from '../prop-types-decorator'
import { computeArraysSum } from '../xo-stats' import { computeArraysSum } from '../xo-stats'
import { formatSize, getMemoryUsedMetric } from '../utils' import {
formatSize,
formatSpeed,
formatTime,
getMemoryUsedMetric,
} from '../utils'
import styles from './index.css' 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,
}}
/>
)
})
)

View File

@ -1495,6 +1495,9 @@ export const deleteSr = sr =>
), ),
}).then(() => _call('sr.destroy', { id: resolveId(sr) }), noop) }).then(() => _call('sr.destroy', { id: resolveId(sr) }), noop)
export const fetchSrStats = (sr, granularity) =>
_call('sr.stats', { id: resolveId(sr), granularity })
export const forgetSr = sr => export const forgetSr = sr =>
confirm({ confirm({
title: _('srForgetModalTitle'), title: _('srForgetModalTitle'),

View File

@ -237,6 +237,18 @@
@extend .fa; @extend .fa;
@extend .fa-database; @extend .fa-database;
} }
&-iops {
@extend .fa;
@extend .fa-cogs;
}
&-latency {
@extend .fa;
@extend .fa-clock-o;
}
&-iowait {
@extend .fa;
@extend .fa-pause;
}
&-delete { &-delete {
@extend .fa; @extend .fa;
@extend .fa-trash; @extend .fa-trash;

View File

@ -20,20 +20,22 @@ import {
} from 'selectors' } from 'selectors'
import TabAdvanced from './tab-advanced' 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 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' import TabXosan from './tab-xosan'
// =================================================================== // ===================================================================
@routes('general', { @routes('general', {
advanced: TabAdvanced, advanced: TabAdvanced,
general: TabGeneral,
logs: TabLogs,
hosts: TabHosts,
disks: TabDisks, disks: TabDisks,
general: TabGeneral,
hosts: TabHosts,
logs: TabLogs,
stats: TabStats,
xosan: TabXosan, xosan: TabXosan,
}) })
@connectStore(() => { @connectStore(() => {
@ -143,6 +145,7 @@ export default class Sr extends Component {
<NavLink to={`/srs/${sr.id}/general`}> <NavLink to={`/srs/${sr.id}/general`}>
{_('generalTabName')} {_('generalTabName')}
</NavLink> </NavLink>
<NavLink to={`/srs/${sr.id}/stats`}>{_('statsTabName')}</NavLink>
<NavLink to={`/srs/${sr.id}/disks`}> <NavLink to={`/srs/${sr.id}/disks`}>
{_('disksTabName', { disks: sr.VDIs.length })} {_('disksTabName', { disks: sr.VDIs.length })}
</NavLink> </NavLink>

View 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>
)
}
}