Skip to content
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

Merged
merged 43 commits into from Mar 20, 2017
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
edc4824
basic render of slave usage page
matush-v Jan 24, 2017
edb96da
code cleanup
matush-v Jan 24, 2017
0dff51f
wired up slave usages to api
matush-v Jan 24, 2017
6b2876c
humans read this
matush-v Jan 24, 2017
60245af
check for real resource values, easily copy stats
matush-v Jan 25, 2017
5f08796
add css
matush-v Jan 25, 2017
96337da
show host name instead of slave id
matush-v Jan 25, 2017
0ffb5d5
better string formatting
matush-v Jan 30, 2017
3c4bf7a
more updates
matush-v Feb 1, 2017
16dccca
better slave usage components
matush-v Feb 1, 2017
c731720
add constants file
matush-v Feb 1, 2017
6ef209c
further component breakdown
matush-v Feb 2, 2017
e8dd569
update packages
matush-v Feb 2, 2017
e8c30dd
fix props
matush-v Feb 2, 2017
e40ee63
fix to work in staging
matush-v Feb 2, 2017
edf3048
dont need no loggin
matush-v Feb 2, 2017
261f9bf
extra api '/' is fixed in staging :|
matush-v Feb 2, 2017
729b4ab
refactoring and styling heatmap towards mockup
matush-v Feb 16, 2017
bb3f187
added aggregates and more refactoring
matush-v Feb 16, 2017
f8bb7aa
forgot name change during git magic
matush-v Feb 16, 2017
fe4cfc0
switched to template literals
matush-v Feb 16, 2017
566f324
aggregate percentages
matush-v Feb 21, 2017
48a5ce6
refactoring and removed task numb check
matush-v Feb 21, 2017
bc5862c
actual timestamp added
matush-v Feb 21, 2017
0db2b17
get active tasks count
matush-v Feb 21, 2017
53d2f3e
move thresholds to config
matush-v Feb 22, 2017
6a3c9b6
add thresholds to config
matush-v Feb 22, 2017
83286ae
ref error
matush-v Feb 22, 2017
c903b6d
only check active slaves
matush-v Feb 23, 2017
09cc66f
slave usage reports are only for active slaves
matush-v Feb 23, 2017
1b4d25e
clarity
matush-v Feb 23, 2017
bd3f960
branches hate me
matush-v Feb 23, 2017
13ed92b
add links to tasks for slave
matush-v Feb 23, 2017
2850ee0
grandient style heatmap for mem and cpu utilization
matush-v Feb 23, 2017
734a636
better clipboard copying
matush-v Feb 23, 2017
f84d57c
remove unused config and styles
matush-v Feb 23, 2017
515141f
sort slave heat map, adjust gradient, refactor
matush-v Feb 24, 2017
a8dec94
extend range to account for hundredths place
matush-v Feb 24, 2017
27388de
add missing getter
ssalinas Mar 1, 2017
8a366de
mouseover callouts, style changes
matush-v Mar 10, 2017
c9fe927
increase margin, tooltip on bottom
matush-v Mar 13, 2017
08445bb
fix merge conflicts with master
ssalinas Mar 20, 2017
a37cdb0
missed merges in package.json
ssalinas Mar 20, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion SingularityUI/app/actions/api/slaves.es6
Expand Up @@ -54,4 +54,7 @@ export const RemoveExpiringSlaveState = buildJsonApiAction(
})
);


export const FetchSlaveUsages = buildApiAction(
'FETCH_SLAVE_USAGES',
{url : '/usage/slaves'}
);
1 change: 1 addition & 0 deletions SingularityUI/app/components/common/Navigation.jsx
Expand Up @@ -62,6 +62,7 @@ const Navigation = (props) => {
<ul className="dropdown-menu">
<li><Link to="/racks">Racks</Link></li>
<li><Link to="/slaves">Slaves</Link></li>
<li><Link to="/slave-usage">Slave Usage</Link></li>
<li><Link to="/webhooks">Webhooks</Link></li>
<li><Link to="/disasters">Disasters</Link></li>
<li role="separator" className="divider"></li>
Expand Down
38 changes: 38 additions & 0 deletions SingularityUI/app/components/machines/Constants.jsx
@@ -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 = {
Copy link
Member

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?

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
];
71 changes: 71 additions & 0 deletions SingularityUI/app/components/machines/SlaveAggregates.jsx
@@ -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;
88 changes: 88 additions & 0 deletions SingularityUI/app/components/machines/SlaveHealth.jsx
@@ -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;
89 changes: 89 additions & 0 deletions SingularityUI/app/components/machines/SlaveHealthMenuItems.js
@@ -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;
67 changes: 67 additions & 0 deletions SingularityUI/app/components/machines/SlaveUsage.jsx
@@ -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()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why an empty Promise.all here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍



export default connect(mapStateToProps, mapDispatchToProps)(rootComponent(SlaveUsage, refresh, true, true, initialize));