New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Slave Usage Monitoring UI #1405
Changes from 25 commits
edc4824
edb96da
0dff51f
6b2876c
60245af
5f08796
96337da
0ffb5d5
3c4bf7a
16dccca
c731720
6ef209c
e8dd569
e8c30dd
e40ee63
edf3048
261f9bf
729b4ab
bb3f187
f8bb7aa
fe4cfc0
566f324
48a5ce6
bc5862c
0db2b17
53d2f3e
6a3c9b6
83286ae
c903b6d
09cc66f
1b4d25e
bd3f960
13ed92b
2850ee0
734a636
f84d57c
515141f
a8dec94
27388de
8a366de
c9fe927
08445bb
a37cdb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
export const HUNDREDTHS_PLACE = 2; | ||
|
||
export const SLAVE_STYLES = { | ||
ok : 'ok-slave', | ||
warning : 'warning-slave', | ||
critical : 'critical-slave' | ||
}; | ||
|
||
export const THRESHOLDS = { | ||
cpusWarningThreshold : 0.80, | ||
cpusCriticalThreshold : 0.90, | ||
memoryWarningThreshold : 0.80, | ||
memoryCriticalThreshold : 0.90, | ||
numTasksWarning : 100, | ||
numTasksCritical : 120 | ||
}; | ||
|
||
export const STAT_NAMES = { | ||
cpusUsedStat : 'cpusUsed', | ||
memoryBytesUsedStat : 'memoryBytesUsed', | ||
numTasksStat : 'numTasks', | ||
slaveIdStat : 'slaveId', | ||
timestampStat : 'timestamp' | ||
}; | ||
|
||
export const STAT_STYLES = { | ||
ok : 'ok-stat', | ||
warning : 'warning-stat', | ||
critical : 'critical-stat' | ||
}; | ||
|
||
export const SLAVE_HEALTH_MENU_ITEM_ORDER = [ | ||
'host', | ||
STAT_NAMES.cpusUsedStat, | ||
STAT_NAMES.memoryBytesUsedStat, | ||
STAT_NAMES.numTasksStat, | ||
STAT_NAMES.timestampStat | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import React, { PropTypes } from 'react'; | ||
import CircularProgressbar from 'react-circular-progressbar'; | ||
import Utils from '../../utils'; | ||
import { STAT_NAMES, HUNDREDTHS_PLACE } from './Constants'; | ||
|
||
const getPctSlaveUsage = (slaves, slaveUsages, usageCallback, resourceCallback) => { | ||
const totalUsage = slaveUsages.map(usageCallback) | ||
.reduce((acc, val) => acc + parseFloat(val), 0); | ||
|
||
const totalResource = slaves.map(resourceCallback) | ||
.reduce((acc, val) => acc + parseFloat(val), 0); | ||
|
||
return Utils.roundTo((totalUsage / totalResource) * 100, HUNDREDTHS_PLACE); | ||
}; | ||
|
||
const getCpuUtilizationPct = (slaves, slaveUsages) => { | ||
return getPctSlaveUsage(slaves, | ||
slaveUsages, | ||
usage => usage.cpusUsed, | ||
slave => Utils.getMaxAvailableResource(slave, STAT_NAMES.cpusUsedStat)); | ||
}; | ||
|
||
const getMemUtilizationPct = (slaves, slaveUsages) => { | ||
return getPctSlaveUsage(slaves, | ||
slaveUsages, | ||
usage => usage.memoryBytesUsed, | ||
slave => Utils.getMaxAvailableResource(slave, STAT_NAMES.memoryBytesUsedStat)); | ||
}; | ||
|
||
const SlaveAggregates = ({slaves, slaveUsages, activeTasks}) => { | ||
return ( | ||
<div className="slave-aggregates row"> | ||
<div className="total-slaves col-xs-3"> | ||
<div id="value"> | ||
{slaves.length} | ||
</div> | ||
<div id="label"> | ||
Total Slaves | ||
</div> | ||
</div> | ||
<div className="total-tasks col-xs-3"> | ||
<div id="value"> | ||
{activeTasks} | ||
</div> | ||
<div id="label"> | ||
Tasks Running | ||
</div> | ||
</div> | ||
<div className="avg-cpu col-xs-3"> | ||
<CircularProgressbar percentage={getCpuUtilizationPct(slaves, slaveUsages)} initialAnimation={true} textForPercentage={(pct) => `${pct}%`} /> | ||
<div id="label"> | ||
Cpu | ||
</div> | ||
</div> | ||
<div className="avg-memory col-xs-3"> | ||
<CircularProgressbar percentage={getMemUtilizationPct(slaves, slaveUsages)} initialAnimation={true} textForPercentage={(pct) => `${pct}%`} /> | ||
<div id="label"> | ||
Memory | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
SlaveAggregates.propTypes = { | ||
slaves : PropTypes.array, | ||
slaveUsages : PropTypes.array, | ||
activeTasks : PropTypes.number.isRequired | ||
}; | ||
|
||
export default SlaveAggregates; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import React, { PropTypes } from 'react'; | ||
import Utils from '../../utils'; | ||
import SlaveHealthMenuItems from './SlaveHealthMenuItems'; | ||
import { Dropdown } from 'react-bootstrap'; | ||
import { SLAVE_STYLES, THRESHOLDS, STAT_NAMES, STAT_STYLES } from './Constants'; | ||
|
||
|
||
const isStatCritical = (slaveInfo, slaveUsage, statName) => { | ||
switch (statName) { | ||
case STAT_NAMES.cpusUsedStat: | ||
return (slaveUsage.cpusUsed / Utils.getMaxAvailableResource(slaveInfo, statName)) > THRESHOLDS.cpusCriticalThreshold; | ||
case STAT_NAMES.memoryBytesUsedStat: | ||
return (slaveUsage.memoryBytesUsed / (Utils.getMaxAvailableResource(slaveInfo, statName))) > THRESHOLDS.memoryCriticalThreshold; | ||
default: | ||
return false; | ||
} | ||
}; | ||
|
||
const isStatWarning = (slaveInfo, slaveUsage, statName) => { | ||
switch (statName) { | ||
case STAT_NAMES.cpusUsedStat: | ||
return (slaveUsage.cpusUsed / Utils.getMaxAvailableResource(slaveInfo, statName)) > THRESHOLDS.cpusWarningThreshold; | ||
case STAT_NAMES.memoryBytesUsedStat: | ||
return (slaveUsage.memoryBytesUsed / (Utils.getMaxAvailableResource(slaveInfo, statName))) > THRESHOLDS.memoryWarningThreshold; | ||
default: | ||
return false; | ||
} | ||
}; | ||
|
||
const getSlaveStyle = (checkedStats) => { | ||
let style = SLAVE_STYLES.ok; | ||
|
||
if (checkedStats.some((obj) => {return obj.style === STAT_STYLES.critical;})) { | ||
style = SLAVE_STYLES.critical; | ||
} else if (checkedStats.some((obj) => {return obj.style === STAT_STYLES.warning;})) { | ||
style = SLAVE_STYLES.warning; | ||
} | ||
|
||
return style; | ||
}; | ||
|
||
const SlaveHealth = ({slaveInfo, slaveUsage}) => { | ||
const checkStats = (val, stat) => { | ||
const newStat = { | ||
name : stat, | ||
value : (stat === STAT_NAMES.slaveIdStat ? slaveInfo.host : val), | ||
maybeTotalResource : Utils.isResourceStat(stat) ? Utils.getMaxAvailableResource(slaveInfo, stat) : '', | ||
style : STAT_STYLES.ok | ||
}; | ||
|
||
if (isStatCritical(slaveInfo, slaveUsage, stat)) { | ||
newStat.style = STAT_STYLES.critical; | ||
} else if (isStatWarning(slaveInfo, slaveUsage, stat)) { | ||
newStat.style = STAT_STYLES.warning; | ||
} | ||
|
||
return newStat; | ||
}; | ||
|
||
const checkedStats = _.map(slaveUsage, checkStats); | ||
|
||
const slaveStyle = getSlaveStyle(checkedStats); | ||
|
||
return ( | ||
<Dropdown key={slaveUsage.slaveId} id={slaveUsage.slaveId}> | ||
<Dropdown.Toggle noCaret={true} className={`${slaveStyle} single-slave-btn`} /> | ||
<Dropdown.Menu> | ||
<SlaveHealthMenuItems stats={checkedStats} /> | ||
</Dropdown.Menu> | ||
</Dropdown> | ||
); | ||
}; | ||
|
||
SlaveHealth.propTypes = { | ||
slaveUsage : PropTypes.shape({ | ||
slaveId : PropTypes.string.isRequired, | ||
cpusUsed : PropTypes.number.isRequired, | ||
memoryBytesUsed : PropTypes.number.isRequired, | ||
numTasks : PropTypes.number.isRequired, | ||
timestamp : PropTypes.number.isRequired | ||
}), | ||
slaveInfo : PropTypes.shape({ | ||
attributes : PropTypes.object.isRequired, | ||
resources : PropTypes.object.isRequired | ||
}) | ||
}; | ||
|
||
export default SlaveHealth; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import React, { PropTypes } from 'react'; | ||
import StatItem from './StatItem'; | ||
import Utils from '../../utils'; | ||
import { STAT_NAMES, SLAVE_HEALTH_MENU_ITEM_ORDER, HUNDREDTHS_PLACE } from './Constants'; | ||
|
||
const compareStats = (a, b) => { | ||
return SLAVE_HEALTH_MENU_ITEM_ORDER.indexOf(a.name) - SLAVE_HEALTH_MENU_ITEM_ORDER.indexOf(b.name); | ||
}; | ||
|
||
const humanizeStatName = (name) => { | ||
switch (name) { | ||
case STAT_NAMES.slaveIdStat: | ||
return 'HOST'; | ||
case STAT_NAMES.cpusUsedStat: | ||
return 'CPU'; | ||
case STAT_NAMES.memoryBytesUsedStat: | ||
return 'MEM'; | ||
case STAT_NAMES.numTasksStat: | ||
return 'TASKS'; | ||
case STAT_NAMES.timestampStat: | ||
return ''; | ||
default: | ||
throw new Error(`${name} is an unsupported statistic`); | ||
} | ||
}; | ||
|
||
const humanizeStatValue = (name, value, maybeTotalResource) => { | ||
switch (name) { | ||
case STAT_NAMES.slaveIdStat: | ||
return Utils.humanizeSlaveHostName(value); | ||
case STAT_NAMES.cpusUsedStat: | ||
return `${Utils.roundTo(value, HUNDREDTHS_PLACE)} / ${maybeTotalResource}`; | ||
case STAT_NAMES.memoryBytesUsedStat: | ||
return `${Utils.humanizeFileSize(value)} / ${Utils.humanizeFileSize(maybeTotalResource)}`; | ||
case STAT_NAMES.numTasksStat: | ||
return value.toString(); | ||
case STAT_NAMES.timestampStat: | ||
return ''; | ||
default: | ||
throw new Error(`${name} is an unsupported statistic`); | ||
} | ||
}; | ||
|
||
const humanizeStatPct = (name, value, maybeTotalResource) => { | ||
if (Utils.isResourceStat(name)) { | ||
return Utils.roundTo((value / maybeTotalResource) * 100, HUNDREDTHS_PLACE); | ||
} | ||
|
||
return null; | ||
}; | ||
|
||
|
||
const SlaveHealthMenuItems = ({stats}) => { | ||
const renderSlaveStats = _.map(stats.sort(compareStats), ({name, value, maybeTotalResource, style}) => { | ||
return <StatItem key={name} name={humanizeStatName(name)} value={humanizeStatValue(name, value, maybeTotalResource)} className={style} percentage={humanizeStatPct(name, value, maybeTotalResource)} />; | ||
}); | ||
|
||
return ( | ||
<div id="slave-stats"> | ||
{renderSlaveStats} | ||
<li className="timestamp-stat"> | ||
<div className="row"> | ||
<div className="col-xs-12"> | ||
Last updated {Utils.timestampFromNow(stats.find((stat) => stat.name === STAT_NAMES.timestampStat).value)} | ||
</div> | ||
</div> | ||
</li> | ||
</div> | ||
); | ||
}; | ||
|
||
SlaveHealthMenuItems.propTypes = { | ||
stats : PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
name : PropTypes.string.isRequired, | ||
value : PropTypes.oneOfType([ | ||
PropTypes.string, | ||
PropTypes.number | ||
]).isRequired, | ||
maybeTotalResource : PropTypes.oneOfType([ | ||
PropTypes.string, | ||
PropTypes.number | ||
]).isRequired, | ||
style : PropTypes.string.isRequired | ||
}) | ||
) | ||
}; | ||
|
||
export default SlaveHealthMenuItems; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import React, { PropTypes } from 'react'; | ||
import { connect } from 'react-redux'; | ||
import rootComponent from '../../rootComponent'; | ||
import { FetchSlaveUsages, FetchSlaves } from '../../actions/api/slaves'; | ||
import { FetchSingularityStatus } from '../../actions/api/state'; | ||
import SlaveAggregates from './SlaveAggregates'; | ||
import SlaveHealth from './SlaveHealth'; | ||
|
||
const getSlaveInfo = (slaves, slaveUsage) => { | ||
return _.findWhere(slaves, {'id' : slaveUsage.slaveId}); | ||
}; | ||
|
||
const SlaveUsage = ({slaves, slaveUsages, activeTasks}) => { | ||
const slaveHealthData = slaveUsages.map((slaveUsage, index) => { | ||
const slaveInfo = getSlaveInfo(slaves, slaveUsage); | ||
return <SlaveHealth key={index} slaveUsage={slaveUsage} slaveInfo={slaveInfo} />; | ||
}); | ||
|
||
return ( | ||
<div id="slave-usage-page"> | ||
<h1>Slave Usage</h1> | ||
<div> | ||
<SlaveAggregates slaves={slaves} slaveUsages={slaveUsages} activeTasks={activeTasks} /> | ||
</div> | ||
<hr /> | ||
<div id="slave-health"> | ||
<h3>Slave health</h3> | ||
{slaveHealthData} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
SlaveUsage.propTypes = { | ||
slaveUsages : PropTypes.arrayOf(PropTypes.object), | ||
slaves : PropTypes.arrayOf(PropTypes.object), | ||
activeTasks : PropTypes.number | ||
}; | ||
|
||
function mapStateToProps(state) { | ||
return { | ||
slaveUsages : state.api.slaveUsages.data, | ||
slaves : state.api.slaves.data, | ||
activeTasks : state.api.status.data.activeTasks | ||
}; | ||
} | ||
|
||
function mapDispatchToProps(dispatch) { | ||
return { | ||
fetchSlaves : () => dispatch(FetchSlaves.trigger()), | ||
fetchSlaveUsages : () => dispatch(FetchSlaveUsages.trigger()), | ||
fetchSingularityStatus : () => dispatch(FetchSingularityStatus.trigger()) | ||
}; | ||
} | ||
|
||
const refresh = () => (dispatch) => | ||
Promise.all([ | ||
dispatch(FetchSlaves.trigger()), | ||
dispatch(FetchSlaveUsages.trigger()), | ||
dispatch(FetchSingularityStatus.trigger()) | ||
]); | ||
|
||
const initialize = () => (dispatch) => | ||
Promise.all([]).then(() => dispatch(refresh())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why an empty Promise.all here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mostly due to my lack of understanding of which pieces are necessary. Looking at the rootComponent code, it looks like not having an initialize will just default to the refresh which is fine so I'll remove this function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
|
||
export default connect(mapStateToProps, mapDispatchToProps)(rootComponent(SlaveUsage, refresh, true, true, initialize)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can load these in from window.config instead?