playback metrics data

fixes #357
This commit is contained in:
JUDIT GRESKOVITS 2017-03-17 19:27:15 +00:00 committed by Sérgio Ramos
parent 9f03c65f05
commit 263d1518af
25 changed files with 1691 additions and 529 deletions

View File

@ -49,7 +49,6 @@
"react/prefer-stateless-function": 2,
"react/self-closing-comp": 2,
"react/sort-comp": 2,
"react/sort-prop-types": 2,
"react/style-prop-object": 2,
"react/jsx-boolean-value": [2, "never"],
"react/jsx-closing-bracket-location": 2,

View File

@ -31,7 +31,6 @@
"lodash.get": "^4.4.2",
"lodash.isempty": "^4.4.0",
"lodash.template": "^4.4.0",
"lodash.uniq": "^4.5.0",
"moment": "^2.17.1",
"param-case": "^2.1.0",
"querystring": "^0.2.0",

View File

@ -1,6 +1,6 @@
import React from 'react';
import MetricsOutlet from '@components/metrics-outlet';
import ItemMetricGroup from '@components/item-metric-group';
import PropTypes from '@root/prop-types';
import {
@ -20,7 +20,9 @@ const InstanceItem = ({
<ListItemMeta onClick={toggleCollapsed}>
<ListItemTitle>{instance.name}</ListItemTitle>
</ListItemMeta>
<MetricsOutlet datasets={instance.metrics} />
<ItemMetricGroup
datasets={instance.metrics}
/>
</ListItemView>
<ListItemOptions>

View File

@ -5,14 +5,7 @@ import Column from '@ui/components/column';
import { ListItemOutlet } from '@ui/components/list';
import PropTypes from '@root/prop-types';
import Row from '@ui/components/row';
import {
MetricGraph,
MiniMetricMeta,
MiniMetricTitle,
MiniMetricSubtitle,
MetricView
} from '@ui/components/metric';
import MetricItem from './item';
const StyledOutlet = styled(ListItemOutlet)`
padding-left: 0;
@ -28,19 +21,15 @@ const StyledRow = styled(Row)`
}
`;
const MetricsOutlet = ({
const ItemMetricGroup = ({
datasets = [],
...props
}) => {
const _datasets = datasets.map((metric, i) => (
<Column key={i} xs={4}>
<MetricView mini borderless>
<MiniMetricMeta>
<MiniMetricTitle>Memory: 54%</MiniMetricTitle>
<MiniMetricSubtitle>(1280/3000 MB)</MiniMetricSubtitle>
</MiniMetricMeta>
<MetricGraph data={metric.data} />
</MetricView>
<Column key={i} xs={12/datasets.length}>
<MetricItem
{...metric}
/>
</Column>
));
@ -53,8 +42,8 @@ const MetricsOutlet = ({
);
};
MetricsOutlet.propTypes = {
ItemMetricGroup.propTypes = {
datasets: React.PropTypes.arrayOf(PropTypes.dataset)
};
export default MetricsOutlet;
export default ItemMetricGroup;

View File

@ -0,0 +1,20 @@
import React from 'react';
import { MetricGraph, MetricView } from '@ui/components/metric';
import PropTypes from '@root/prop-types';
const MetricItem = ({
uuid,
data
}) => (
<MetricView borderless mini>
<MetricGraph data={data} />
</MetricView>
);
MetricItem.propTypes = {
uuid: React.PropTypes.string,
data: PropTypes.data
};
export default MetricItem;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components';
import { Link } from '@ui/components/anchor';
import MetricsOutlet from '@components/metrics-outlet';
import ItemMetricGroup from '@components/item-metric-group';
import { Checkbox, FormGroup } from '@ui/components/form';
import PropTypes from '@root/prop-types';
import forceArray from 'force-array';
@ -101,7 +101,9 @@ const ServiceItem = ({
{isChild && subtitle}
{description}
</ListItemMeta>
<MetricsOutlet datasets={service.metrics} />
<ItemMetricGroup
datasets={service.metrics}
/>
</ListItemView>
);

View File

@ -21,7 +21,6 @@ const Services = (props) => {
push
} = props;
// TODO: Move into "components" and fix absolute
// positioning on responsive screens
const instances = (instances = 0) => {
@ -33,6 +32,7 @@ const Services = (props) => {
`;
if ( instances.length <= 0 || instances <= 0 ) return;
return (
<StyledButton tertiary>
You have 5 instances
@ -40,13 +40,15 @@ const Services = (props) => {
);
};
const toggleValue = path === '/:org/projects/:projectId/services' ?
'topology' : 'list';
const toggleValue = path === '/:org/projects/:projectId/services'
? 'topology'
: 'list';
const onToggle = (value) => {
const path = `/${org.id}/projects/${project.id}/services${
value === 'list' ? '/list' : ''
}`;
push(path);
};
@ -56,7 +58,7 @@ const Services = (props) => {
toggleValue={toggleValue}
services={services}
>
{ instances() }
{instances()}
{children}
</ServicesView>
);

View File

@ -6,6 +6,7 @@ import ServiceItem from '@components/service/item';
import UnmanagedInstances from '@components/services/unmanaged-instances';
import { toggleTooltip } from '@state/actions';
import ServicesTooltip from '@components/services/tooltip';
import { subscribeMetric } from '@state/thunks';
import {
orgByIdSelector,
@ -18,8 +19,17 @@ const StyledContainer = styled.div`
position: relative;
`;
// TMP - single source of truth
const duration = '1 hour';
const interval = '2 minutes';
class Services extends React.Component {
// we DON'T want to unsubscribe once we started going
componentWillMount() {
this.props.subscribeMetric(interval);
}
ref(name) {
this._refs = this._refs || {};
@ -33,7 +43,7 @@ class Services extends React.Component {
org = {},
project = {},
services = [],
toggleTooltip = (() => {}),
toggleTooltip = () => ({}),
uiTooltip = {}
} = this.props;
@ -87,7 +97,8 @@ Services.propTypes = {
project: PropTypes.project,
services: React.PropTypes.arrayOf(PropTypes.service),
toggleTooltip: React.PropTypes.func,
uiTooltip: React.PropTypes.object
uiTooltip: React.PropTypes.object,
subscribeMetric: React.PropTypes.func
};
const mapStateToProps = (state, {
@ -98,12 +109,16 @@ const mapStateToProps = (state, {
}) => ({
org: orgByIdSelector(match.params.org)(state),
project: projectByIdSelector(match.params.projectId)(state),
services: servicesByProjectIdSelector(match.params.projectId)(state),
services: servicesByProjectIdSelector(match.params.projectId, {
duration,
interval
})(state),
uiTooltip: serviceUiTooltipSelector(state)
});
const mapDispatchToProps = (dispatch) => ({
toggleTooltip: (data) => dispatch(toggleTooltip(data))
toggleTooltip: (data) => dispatch(toggleTooltip(data)),
subscribeMetric: (payload) => dispatch(subscribeMetric(payload))
});
export default connect(

1144
frontend/src/datasets.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import React from 'react';
import App from '@containers/app';
import MockState from './mock-state.json';
import Datasets from './datasets.json';
import Store from '@state/store';
if (process.env.NODE_ENV !== 'production') {
@ -15,6 +16,24 @@ if (process.env.NODE_ENV !== 'production') {
});
}
// TMP - ensure datasets are at least 2 hrs long - START
import getTwoHourDatasets from './utils/two-hour-metric-datasets';
const twoHourLongDatasets = getTwoHourDatasets(Datasets);
// TMP - ensure datasets are at least 2 hrs long - END
// TMP - plug fake metric data - START
const datasets = MockState.metrics.data.datasets.map((dataset, index) => {
const keyIndex = index%2 ? 0 : 1;
const key = Object.keys(twoHourLongDatasets)[keyIndex];
return {
...dataset,
data: twoHourLongDatasets[key]
};
});
MockState.metrics.data.datasets = datasets;
// TMP - plug fake metric data - END
ReactDOM.render(
<Provider store={Store(MockState)}>
<IntlProvider>

View File

@ -53,6 +53,7 @@
},
"metrics": {
"ui": {
"pos": 0,
"durations": [
"360",
"720",
@ -163,317 +164,11 @@
"datasets": [{
"type": "2aaa237d-42b3-442f-9094-a17aa470014b",
"uuid": "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec",
"data": [{
"firstQuartile": 15,
"thirdQuartile": 15,
"median": 15,
"max": 15,
"min": 15
}, {
"firstQuartile": 26,
"thirdQuartile": 26,
"median": 26,
"max": 26,
"min": 26
}, {
"firstQuartile": 17,
"thirdQuartile": 17,
"median": 17,
"max": 17,
"min": 17
}, {
"firstQuartile": 15,
"thirdQuartile": 25,
"median": 19,
"max": 19,
"min": 20
}, {
"firstQuartile": 19,
"thirdQuartile": 25,
"median": 21,
"max": 20,
"min": 25
}, {
"firstQuartile": 24,
"thirdQuartile": 30,
"median": 25,
"max": 26,
"min": 27
}, {
"firstQuartile": 28,
"thirdQuartile": 34,
"median": 30,
"max": 30,
"min": 30
}, {
"firstQuartile": 30,
"thirdQuartile": 45,
"median": 35,
"max": 40,
"min": 40
}, {
"firstQuartile": 20,
"thirdQuartile": 55,
"median": 45,
"max": 44,
"min": 44
}, {
"firstQuartile": 55,
"thirdQuartile": 55,
"median": 55,
"max": 55,
"min": 55
}, {
"firstQuartile": 57,
"thirdQuartile": 56,
"median": 57,
"max": 58,
"min": 57
}, {
"firstQuartile": 57,
"thirdQuartile": 56,
"median": 56,
"max": 56,
"min": 56
}, {
"firstQuartile": 60,
"thirdQuartile": 56,
"median": 60,
"max": 60,
"min": 60
}, {
"firstQuartile": 57,
"thirdQuartile": 57,
"median": 57,
"max": 57,
"min": 57
}, {
"firstQuartile": 57,
"thirdQuartile": 55,
"median": 55,
"max": 55,
"min": 55
}, {
"firstQuartile": 20,
"thirdQuartile": 45,
"median": 45,
"max": 45,
"min": 45
}, {
"firstQuartile": 15,
"thirdQuartile": 40,
"median": 30,
"max": 49,
"min": 30
}, {
"firstQuartile": 15,
"thirdQuartile": 15,
"median": 15,
"max": 15,
"min": 15
}, {
"firstQuartile": 26,
"thirdQuartile": 26,
"median": 26,
"max": 26,
"min": 26
}, {
"firstQuartile": 17,
"thirdQuartile": 17,
"median": 17,
"max": 17,
"min": 17
}, {
"firstQuartile": 15,
"thirdQuartile": 25,
"median": 19,
"max": 19,
"min": 20
}, {
"firstQuartile": 19,
"thirdQuartile": 25,
"median": 21,
"max": 20,
"min": 25
}, {
"firstQuartile": 24,
"thirdQuartile": 30,
"median": 25,
"max": 26,
"min": 10
}, {
"firstQuartile": 28,
"thirdQuartile": 34,
"median": 30,
"max": 30,
"min": 30
}, {
"firstQuartile": 30,
"thirdQuartile": 45,
"median": 35,
"max": 40,
"min": 40
}, {
"firstQuartile": 20,
"thirdQuartile": 55,
"median": 45,
"max": 44,
"min": 44
}, {
"firstQuartile": 55,
"thirdQuartile": 55,
"median": 55,
"max": 55,
"min": 55
}, {
"firstQuartile": 57,
"thirdQuartile": 56,
"median": 57,
"max": 58,
"min": 57
}, {
"firstQuartile": 57,
"thirdQuartile": 56,
"median": 56,
"max": 56,
"min": 56
}, {
"firstQuartile": 60,
"thirdQuartile": 56,
"median": 60,
"max": 60,
"min": 60
}, {
"firstQuartile": 57,
"thirdQuartile": 57,
"median": 57,
"max": 57,
"min": 57
}, {
"firstQuartile": 57,
"thirdQuartile": 55,
"median": 55,
"max": 55,
"min": 55
}, {
"firstQuartile": 20,
"thirdQuartile": 45,
"median": 45,
"max": 45,
"min": 45
}, {
"firstQuartile": 15,
"thirdQuartile": 40,
"median": 30,
"max": 49,
"min": 30
}, {
"firstQuartile": 15,
"thirdQuartile": 15,
"median": 15,
"max": 15,
"min": 15
}, {
"firstQuartile": 26,
"thirdQuartile": 26,
"median": 26,
"max": 26,
"min": 26
}, {
"firstQuartile": 17,
"thirdQuartile": 17,
"median": 17,
"max": 17,
"min": 17
}, {
"firstQuartile": 15,
"thirdQuartile": 25,
"median": 19,
"max": 19,
"min": 20
}, {
"firstQuartile": 19,
"thirdQuartile": 25,
"median": 21,
"max": 20,
"min": 25
}, {
"firstQuartile": 24,
"thirdQuartile": 30,
"median": 25,
"max": 26,
"min": 27
}, {
"firstQuartile": 28,
"thirdQuartile": 34,
"median": 30,
"max": 30,
"min": 30
}, {
"firstQuartile": 30,
"thirdQuartile": 45,
"median": 35,
"max": 40,
"min": 40
}, {
"firstQuartile": 20,
"thirdQuartile": 55,
"median": 45,
"max": 44,
"min": 44
}, {
"firstQuartile": 55,
"thirdQuartile": 55,
"median": 55,
"max": 55,
"min": 55
}, {
"firstQuartile": 57,
"thirdQuartile": 56,
"median": 57,
"max": 58,
"min": 57
}, {
"firstQuartile": 57,
"thirdQuartile": 56,
"median": 56,
"max": 56,
"min": 56
}, {
"firstQuartile": 60,
"thirdQuartile": 56,
"median": 60,
"max": 60,
"min": 60
}, {
"firstQuartile": 57,
"thirdQuartile": 57,
"median": 57,
"max": 57,
"min": 57
}, {
"firstQuartile": 57,
"thirdQuartile": 55,
"median": 55,
"max": 55,
"min": 55
}, {
"firstQuartile": 20,
"thirdQuartile": 45,
"median": 45,
"max": 45,
"min": 45
}, {
"firstQuartile": 15,
"thirdQuartile": 40,
"median": 30,
"max": 49,
"min": 30
}]
"data": []
},{
"type": "dca08514-72e5-46ce-ad91-e68b3b0914d9",
"type": "dca08514-72e5-46ce-ad92-e68b3b0914d4",
"uuid": "4e6ee79a-7453-4fc6-b9da-7ae1e41138ed",
"data": [{"firstQuartile":1.62,"thirdQuartile":1.62,"median":1.62,"max":1.62,"min":1.62},{"firstQuartile":1.67,"thirdQuartile":1.67,"median":1.67,"max":1.67,"min":1.67},{"firstQuartile":1.63,"thirdQuartile":1.63,"median":1.63,"max":1.63,"min":1.63},{"firstQuartile":1.62,"thirdQuartile":1.66,"median":1.64,"max":1.64,"min":1.64},{"firstQuartile":1.64,"thirdQuartile":1.66,"median":1.64,"max":1.64,"min":1.66},{"firstQuartile":1.66,"thirdQuartile":1.69,"median":1.66,"max":1.67,"min":1.67},{"firstQuartile":1.68,"thirdQuartile":1.70,"median":1.69,"max":1.69,"min":1.69},{"firstQuartile":1.69,"thirdQuartile":1.75,"median":1.71,"max":1.73,"min":1.73},{"firstQuartile":1.64,"thirdQuartile":1.80,"median":1.75,"max":1.75,"min":1.75},{"firstQuartile":1.80,"thirdQuartile":1.80,"median":1.80,"max":1.80,"min":1.80},{"firstQuartile":1.81,"thirdQuartile":1.80,"median":1.81,"max":1.81,"min":1.81},{"firstQuartile":1.81,"thirdQuartile":1.80,"median":1.80,"max":1.80,"min":1.80},{"firstQuartile":1.82,"thirdQuartile":1.80,"median":1.82,"max":1.82,"min":1.82},{"firstQuartile":1.81,"thirdQuartile":1.81,"median":1.81,"max":1.81,"min":1.81},{"firstQuartile":1.81,"thirdQuartile":1.80,"median":1.80,"max":1.80,"min":1.80},{"firstQuartile":1.64,"thirdQuartile":1.75,"median":1.75,"max":1.75,"min":1.75},{"firstQuartile":1.62,"thirdQuartile":1.73,"median":1.69,"max":1.77,"min":1.69},{"firstQuartile":1.62,"thirdQuartile":1.62,"median":1.62,"max":1.62,"min":1.62},{"firstQuartile":1.67,"thirdQuartile":1.67,"median":1.67,"max":1.67,"min":1.67},{"firstQuartile":1.63,"thirdQuartile":1.63,"median":1.63,"max":1.63,"min":1.63},{"firstQuartile":1.62,"thirdQuartile":1.66,"median":1.64,"max":1.64,"min":1.64},{"firstQuartile":1.64,"thirdQuartile":1.66,"median":1.64,"max":1.64,"min":1.66},{"firstQuartile":1.66,"thirdQuartile":1.69,"median":1.66,"max":1.67,"min":1.59},{"firstQuartile":1.68,"thirdQuartile":1.70,"median":1.69,"max":1.69,"min":1.69},{"firstQuartile":1.69,"thirdQuartile":1.75,"median":1.71,"max":1.73,"min":1.73},{"firstQuartile":1.64,"thirdQuartile":1.80,"median":1.75,"max":1.75,"min":1.75},{"firstQuartile":1.80,"thirdQuartile":1.80,"median":1.80,"max":1.80,"min":1.80},{"firstQuartile":1.81,"thirdQuartile":1.80,"median":1.81,"max":1.81,"min":1.81},{"firstQuartile":1.81,"thirdQuartile":1.80,"median":1.80,"max":1.80,"min":1.80},{"firstQuartile":1.82,"thirdQuartile":1.80,"median":1.82,"max":1.82,"min":1.82},{"firstQuartile":1.81,"thirdQuartile":1.81,"median":1.81,"max":1.81,"min":1.81},{"firstQuartile":1.81,"thirdQuartile":1.80,"median":1.80,"max":1.80,"min":1.80},{"firstQuartile":1.64,"thirdQuartile":1.75,"median":1.75,"max":1.75,"min":1.75},{"firstQuartile":1.62,"thirdQuartile":1.73,"median":1.69,"max":1.77,"min":1.69},{"firstQuartile":1.62,"thirdQuartile":1.62,"median":1.62,"max":1.62,"min":1.62},{"firstQuartile":1.67,"thirdQuartile":1.67,"median":1.67,"max":1.67,"min":1.67},{"firstQuartile":1.63,"thirdQuartile":1.63,"median":1.63,"max":1.63,"min":1.63},{"firstQuartile":1.62,"thirdQuartile":1.66,"median":1.64,"max":1.64,"min":1.64},{"firstQuartile":1.64,"thirdQuartile":1.66,"median":1.64,"max":1.64,"min":1.66},{"firstQuartile":1.66,"thirdQuartile":1.69,"median":1.66,"max":1.67,"min":1.67},{"firstQuartile":1.68,"thirdQuartile":1.70,"median":1.69,"max":1.69,"min":1.69},{"firstQuartile":1.69,"thirdQuartile":1.75,"median":1.71,"max":1.73,"min":1.73},{"firstQuartile":1.64,"thirdQuartile":1.80,"median":1.75,"max":1.75,"min":1.75},{"firstQuartile":1.80,"thirdQuartile":1.80,"median":1.80,"max":1.80,"min":1.80},{"firstQuartile":1.81,"thirdQuartile":1.80,"median":1.81,"max":1.81,"min":1.81},{"firstQuartile":1.81,"thirdQuartile":1.80,"median":1.80,"max":1.80,"min":1.80},{"firstQuartile":1.82,"thirdQuartile":1.80,"median":1.82,"max":1.82,"min":1.82},{"firstQuartile":1.81,"thirdQuartile":1.81,"median":1.81,"max":1.81,"min":1.81},{"firstQuartile":1.81,"thirdQuartile":1.80,"median":1.80,"max":1.80,"min":1.80},{"firstQuartile":1.64,"thirdQuartile":1.75,"median":1.75,"max":1.75,"min":1.75},{"firstQuartile":1.62,"thirdQuartile":1.73,"median":1.69,"max":1.77,"min":1.69}]
"data": []
}]
}
},

View File

@ -46,18 +46,18 @@ const MetricType = React.PropTypes.shape({
...BaseObject
});
const Dataset = React.PropTypes.shape({
uuid: React.PropTypes.string,
type: MetricType,
data: React.PropTypes.arrayOf(
React.PropTypes.shape({
const Data = React.PropTypes.shape({
firstQuartile: React.PropTypes.number,
thirdQuartile: React.PropTypes.number,
median: React.PropTypes.number,
max: React.PropTypes.number,
min: React.PropTypes.number
})
)
});
const Dataset = React.PropTypes.shape({
uuid: React.PropTypes.string,
type: MetricType,
data: React.PropTypes.arrayOf(Data)
});
const Sections = React.PropTypes.arrayOf(
@ -74,5 +74,6 @@ export default {
instance: Instance,
metric: Metric,
metricType: MetricType,
dataset: Dataset
dataset: Dataset,
data: Data
};

View File

@ -1,6 +1,5 @@
import constantCase from 'constant-case';
import { createAction } from 'redux-actions';
// import thunks from '@state/thunks';
const APP = constantCase(process.env['APP_NAME']);
@ -54,3 +53,5 @@ export const handleNewProject =
createAction(`${APP}/CREATE_NEW_PROJECT`);
export const toggleTooltip =
createAction(`${APP}/TOGGLE_QUICK_ACTIONS_TOOLTIP`);
export const refreshMetrics =
createAction(`${APP}/REFRESH_METRICS`);

View File

@ -1,9 +1,8 @@
import { handleActions } from 'redux-actions';
import { metricDurationChange } from '@state/actions';
import { metricDurationChange, refreshMetrics } from '@state/actions';
export default handleActions({
[metricDurationChange.toString()]: (state, action) => {
return ({
[metricDurationChange.toString()]: (state, action) => ({
...state,
ui: {
...state.ui,
@ -12,6 +11,15 @@ export default handleActions({
duration: action.payload.duration
}
}
}),
[refreshMetrics.toString()]: (state) => {
return ({
...state,
ui: {
...state.ui,
pos: state.ui.pos + 1
}
});
}
}, {});

View File

@ -66,14 +66,207 @@ const orgSections = (orgId) => createSelector(
const isCollapsed = (collapsed, uuid) => collapsed.indexOf(uuid) >= 0;
const datasets = (metricsData, serviceOrInstanceMetrics, metricsUI) =>
serviceOrInstanceMetrics.map((soim) => ({
...find(metricsData.datasets, ['uuid', soim.dataset]),
const metricByInterval = (data = [], {
min = 0,
max = 100
}, {
duration = '1 hour',
interval = '5 minutes'
}) => {
const getDurationArgs = (value) => {
const [durationNumber, durationType] = value.split(/\s/);
return [Number(durationNumber), durationType];
};
const _duration = moment.duration(...getDurationArgs(duration));
const _interval = moment.duration(...getDurationArgs(interval));
const roundUpDate = (date) => {
const mod = date.valueOf() % _interval.valueOf();
const diff = moment.duration(_interval.valueOf() - mod);
return moment(date).add(diff);
};
const roundDownDate = (date) => {
const mod = date.valueOf() % _interval.valueOf();
return moment(date).subtract(mod);
};
const lastDate = roundUpDate(moment(data[data.length - 1][0], 'X'));
const firstDate = moment(data[0][0], 'X');
const getStart = () => {
const hardStart = moment(lastDate).subtract(_duration);
return hardStart.isBefore(firstDate)
? roundDownDate(firstDate)
: roundUpDate(hardStart);
};
const _start = getStart();
const genSample = (start) => ({
start: start,
end: moment(start).add(_interval),
values: []
});
const genStats = (sample) => {
const data = sample.values.map((r) => r.v);
return {
start: sample.start.valueOf(),
end: sample.end.valueOf(),
firstQuartile: statistics.quantile(data, 0.25),
median: statistics.median(data),
thirdQuartile: statistics.quantile(data, 0.75),
max: statistics.max(data),
min: statistics.min(data),
stddev: statistics.sampleStandardDeviation(data)
};
};
const intervals = data.reduce((samples, value, i) => {
const sample = samples[samples.length - 1];
const previousSample = samples[samples.length - 2];
const record = {
v: Number(value[1]),
t: moment(value[0], 'X')
};
if (record.t.isBefore(_start)) {
return samples;
}
const split = () => {
const stats = genStats(sample);
const nextSample = {
...genSample(sample.end),
values: [record]
};
samples[samples.length - 1] = {
...sample,
stats
};
return [
...samples,
nextSample
];
};
const append = (sample) => {
samples[samples.length - 1] = {
...sample,
values: [...sample.values, record]
};
return samples;
};
const isWithin = (
record.t.isSameOrAfter(sample.start) &&
record.t.isSameOrBefore(sample.end)
);
const isBefore = record.t.isBefore(sample.start);
const isAfter = record.t.isAfter(sample.end);
let newSamples = samples;
if (isWithin) {
newSamples = append(sample);
}
if (isBefore) {
newSamples = append(previousSample);
}
if (isAfter) {
newSamples = split();
}
if ((i + 1) >= data.length) {
const thisSample = newSamples[newSamples.length - 1];
const lastStats = genStats(thisSample);
newSamples[newSamples.length - 1] = {
...thisSample,
stats: lastStats
};
}
return newSamples;
}, [
genSample(_start)
]);
return {
start: _start.valueOf(),
end: lastDate.valueOf(),
duration: _duration.valueOf(),
interval: _interval.valueOf(),
min: min,
max: max,
values: intervals.map((sample) => sample.stats),
__intervals: IS_TEST ? intervals : []
};
};
// TMP - get min and max for total data - START
const getMinMax = (data) => {
const values = data.map((d) => Number(d[1]));
const min = statistics.min(values);
const max = statistics.max(values);
return {
min,
max
};
};
// TMP - get min and max for total dataset - END
// TMP - dataset playback - START
import { getDurationMilliseconds } from '../utils/duration-interval';
const getDataSubset = (data, merticsUI, metricOptions) => {
const duration = getDurationMilliseconds(metricOptions.duration)/1000;
const interval = getDurationMilliseconds(metricOptions.interval)/1000;
const start = data[0][0] + interval*merticsUI.pos;
const end = start + duration;
return data.reduce((acc, d) => {
if(d[0] >= start && d[0] <= end) {
acc.push(d);
}
return acc;
}, []);
};
// TMP - dataset playback - END
const datasets = (
metricsData,
serviceOrInstanceMetrics,
metricsUI,
metricOptions = {
duration: '1 hour',
interval: '2 minutes'
}
) =>
serviceOrInstanceMetrics.map((soim) => {
const dataset = find(metricsData.datasets, ['uuid', soim.dataset]);
const dataSubset = getDataSubset(dataset.data, metricsUI, metricOptions);
const minMax = getMinMax(dataset.data);
return ({
...dataset,
data: metricByInterval(dataSubset, minMax, metricOptions),
type: find(metricsData.types, ['uuid', soim.type]),
...metricsUI[soim.dataset]
}));
});
});
const servicesByProjectId = (projectId) => createSelector(
const servicesByProjectId = (projectId, metricOptions) => createSelector(
[services, projectById(projectId), collapsedServices, metricsData, metricsUI],
(services, project, collapsed, metrics, metricsUI) =>
services.filter((s) => s.project === project.uuid)
@ -84,7 +277,7 @@ const servicesByProjectId = (projectId) => createSelector(
.filter((s) => !s.parent)
.map((service) => ({
...service,
metrics: datasets(metrics, service.metrics, metricsUI),
metrics: datasets(metrics, service.metrics, metricsUI, metricOptions),
collapsed: isCollapsed(collapsed, service.uuid),
services: service.services.map((service) => ({
...service,
@ -229,161 +422,6 @@ const peopleByProjectId = (projectId) => createSelector(
}
);
const metricByInterval = (data = [], {
duration = '1 hour',
interval = '5 minutes'
}) => {
const getDurationArgs = (value) => {
const [durationNumber, durationType] = value.split(/\s/);
return [Number(durationNumber), durationType];
};
const _duration = moment.duration(...getDurationArgs(duration));
const _interval = moment.duration(...getDurationArgs(interval));
const roundUpDate = (date) => {
const mod = date.valueOf() % _interval.valueOf();
const diff = moment.duration(_interval.valueOf() - mod);
return moment(date).add(diff);
};
const roundDownDate = (date) => {
const mod = date.valueOf() % _interval.valueOf();
return moment(date).subtract(mod);
};
const lastDate = roundUpDate(moment(data[data.length - 1][0], 'X'));
const firstDate = moment(data[0][0], 'X');
const getStart = () => {
const hardStart = moment(lastDate).subtract(_duration);
return hardStart.isBefore(firstDate)
? roundDownDate(firstDate)
: roundUpDate(hardStart);
};
const _start = getStart();
const genSample = (start) => ({
start: start,
end: moment(start).add(_interval),
values: []
});
const genStats = (sample) => {
const data = sample.values.map((r) => r.v);
return {
start: sample.start.valueOf(),
end: sample.end.valueOf(),
firstQuartile: statistics.quantile(data, 0.25),
median: statistics.median(data),
thirdQuartile: statistics.quantile(data, 0.75),
max: statistics.max(data),
min: statistics.min(data),
stddev: statistics.sampleStandardDeviation(data)
};
};
const intervals = data.reduce((samples, value, i) => {
const sample = samples[samples.length - 1];
const previousSample = samples[samples.length - 2];
const record = {
v: Number(value[1]),
t: moment(value[0], 'X')
};
if (record.t.isBefore(_start)) {
return samples;
}
const split = () => {
const stats = genStats(sample);
const nextSample = {
...genSample(sample.end),
values: [record]
};
samples[samples.length - 1] = {
...sample,
stats
};
return [
...samples,
nextSample
];
};
const append = (sample) => {
samples[samples.length - 1] = {
...sample,
values: [...sample.values, record]
};
return samples;
};
const isWithin = (
record.t.isSameOrAfter(sample.start) &&
record.t.isSameOrBefore(sample.end)
);
const isBefore = record.t.isBefore(sample.start);
const isAfter = record.t.isAfter(sample.end);
let newSamples = samples;
if (isWithin) {
newSamples = append(sample);
}
if (isBefore) {
newSamples = append(previousSample);
}
if (isAfter) {
newSamples = split();
}
if ((i + 1) >= data.length) {
const thisSample = newSamples[newSamples.length - 1];
const lastStats = genStats(thisSample);
newSamples[newSamples.length - 1] = {
...thisSample,
stats: lastStats
};
}
return newSamples;
}, [
genSample(_start)
]);
// TMP for min / max
const allValues = intervals.reduce((stats, sample) => {
const sampleValues = sample.values.map((value) => value.v);
return stats.concat(sampleValues);
},[]);
const min = statistics.min(allValues);
const max = statistics.max(allValues);
return {
start: _start.valueOf(),
end: lastDate.valueOf(),
duration: _duration.valueOf(),
interval: _interval.valueOf(),
min: min,
max: max,
values: intervals.map((sample) => sample.stats),
__intervals: IS_TEST ? intervals : []
};
};
export {
account as accountSelector,
accountUi as accountUISelector,

View File

@ -1,3 +1,4 @@
// import app from '@state/thunks/app';
export {};
export {
subscribe as subscribeMetric,
unsubscribe as unsubscribeMetric
} from './metrics';

View File

@ -0,0 +1,24 @@
import { getDurationMilliseconds } from '../../utils/duration-interval';
import { refreshMetrics } from '../actions';
let timeoutId = null;
const tick = (dispatch) => {
dispatch(refreshMetrics());
};
export const subscribe = (interval) => (dispatch) => {
if(timeoutId) {
clearTimeout(timeoutId);
}
const timeout = interval ?
getDurationMilliseconds(interval) :
120 * 1000;
timeoutId = setTimeout(tick, timeout, dispatch);
};
export const unsubscribe = () => () => {
if(timeoutId) {
clearTimeout(timeoutId);
}
};

View File

@ -0,0 +1,15 @@
import moment from 'moment';
const getDurationArgs = (value) => {
const [durationNumber, durationType] = value.split(/\s/);
return [Number(durationNumber), durationType];
};
const getDurationMilliseconds = (value) => {
return moment.duration(...getDurationArgs(value)).valueOf();
};
export {
getDurationArgs,
getDurationMilliseconds
};

View File

@ -0,0 +1,49 @@
/* eslint-disable */
// This needs to be at least two hours long
// We need to establish the start time and calculate what the end time is - two hours later
// Then, add the dataset to the array, and calculate its duration
// Then keep adding the dataset to the array, updating each values timstamp with the duration diff * adding,
// until we have data that's at least two hours long
/* eslint-enable */
import moment from 'moment';
const getTwoHourDatasets = (Datasets) => {
return Object.keys(Datasets).reduce((datasets, key) => {
const dataset = Datasets[key];
const datasetStart = moment(dataset[0][0], 'X');
const datasetEnd = moment(dataset[dataset.length - 1][0], 'X');
const datasetDuration = moment(datasetEnd.valueOf())
.subtract(datasetStart.valueOf()).valueOf();
// number of times we need to add the dataset
// so that it's at least 2 hrs long
const count = Math.ceil(moment.duration(2, 'hours')
.valueOf()/datasetDuration);
// update each data's timestamp depending on round of being added
const getDataset = (dataset, duration, iterationIndex) => {
return dataset.map((dataset) => {
const timestamp = dataset[0] + duration*iterationIndex;
return [
timestamp,
dataset[1]
];
});
};
const datasetDurationSec = datasetDuration/1000;
let twoHourDataset = [];
let i = 0;
while(i++ < count) {
const ds = getDataset(Datasets[key], datasetDurationSec, i);
twoHourDataset = twoHourDataset.concat(ds);
}
datasets[key] = twoHourDataset;
return datasets;
}, {});
};
export default getTwoHourDatasets;

View File

@ -2,13 +2,15 @@ const flatten = require('lodash.flatten');
const {
metricByIntervalSelector
} = require('@state/selectors');
const data = require('./metric-by-interval-selector.json');
const moment = require('moment');
const test = require('ava');
const data = require('./metric-by-Interval-selector.json');
test('should ouput the right properties', (t) => {
const stats = metricByIntervalSelector(data, {
min: 0,
max: 100
}, {
duration: '10 minutes',
interval: '30 seconds'
});
@ -33,6 +35,9 @@ test('should ouput the right properties', (t) => {
test('should respect order of records', (t) => {
const stats = metricByIntervalSelector(data, {
min: 0,
max: 100
}, {
duration: '10 minutes',
interval: '30 seconds'
});
@ -47,6 +52,9 @@ test('should respect order of records', (t) => {
test('should respect the intervals', (t) => {
const stats = metricByIntervalSelector(data, {
min: 0,
max: 100
}, {
duration: '10 minutes',
interval: '30 seconds'
});
@ -71,6 +79,9 @@ test('should respect the intervals', (t) => {
test('should respect the intervals', (t) => {
const stats = metricByIntervalSelector(data, {
min: 0,
max: 100
}, {
duration: '10 minutes',
interval: '30 seconds'
});
@ -95,6 +106,9 @@ test('should respect the intervals', (t) => {
test('records should be within intervals', (t) => {
const stats = metricByIntervalSelector(data, {
min: 0,
max: 100
}, {
duration: '10 minutes',
interval: '30 seconds'
});
@ -111,11 +125,17 @@ test('different data chunks should produce almost the same stats', (t) => {
const halfData = data.slice(Math.floor(data.length / 2), data.length);
const stats1 = metricByIntervalSelector(data, {
min: 0,
max: 100
}, {
duration: '10 minutes',
interval: '30 seconds'
});
const stats2 = metricByIntervalSelector(halfData, {
min: 0,
max: 100
}, {
duration: '10 minutes',
interval: '30 seconds'
});

View File

@ -4685,7 +4685,7 @@ lodash.templatesettings@^4.0.0:
dependencies:
lodash._reinterpolate "~3.0.0"
lodash.uniq@^4.3.0, lodash.uniq@^4.5.0:
lodash.uniq@^4.3.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"

View File

@ -0,0 +1,17 @@
scrape_configs:
- job_name: 'leak-fast'
scrape_interval: 15s
static_configs:
- targets: ['fast-node:8000', 'another-fast-node:8000']
- job_name: 'leak-slow'
scrape_interval: 15s
static_configs:
- targets: ['slow-node:8000']
- job_name: 'no-leak'
scrape_interval: 15s
static_configs:
- targets: ['plain-node:8000']
# - job_name: 'leak'
# scrape_interval: 1s
# static_configs:
# - targets: ['fast-node:8000', 'another-fast-node:8000', 'slow-node:8000', 'plain-node:8000']

View File

@ -0,0 +1,103 @@
const relativeDate = require('relative-date');
const statistics = require('simple-statistics');
const prometheus = require('../../scripts/prometheus');
const async = require('async');
const cdm = {};
const calc = (sample) => {
return {
firstQuartile: statistics.quantile(sample, 0.25),
median: statistics.median(sample),
thirdQuartile: statistics.quantile(sample, 0.75),
max: statistics.max(sample),
min: statistics.min(sample),
stddev: statistics.sampleStandardDeviation(sample)
};
};
const getMem = ({
job
}, fn) => {
prometheus.query({
query: [`node_memory_heap_used_bytes{job="${job}"}`]
}).then((res) => {
if (!res || !res[job]) {
return null
}
const aggregate = calc(Object.keys(res[job]).map((inst) => {
return Number(res[job][inst].node_memory_heap_used_bytes[1]);
}));
const instances = Object.keys(res[job]).reduce((sum, inst) => {
return Object.assign(sum, {
[inst]: calc([Number(res[job][inst].node_memory_heap_used_bytes[1])])
})
}, {});
return {
raw: res[job],
aggregate,
instances
};
}).then((res) => {
return fn(null, res);
}).catch((err) => {
return fn(err);
});
};
const getStats = (ctx, fn) => {
async.parallel({
mem: async.apply(getMem, ctx)
}, fn);
};
module.exports = (server) => ({
on: (job) => {
console.log('on', job);
if (cdm[job] && (cdm[job].sockets > 0)) {
cdm[job].sockets += 1;
return;
}
let messageId = 0;
const update = () => {
console.log(`publishing /stats/${job}/${messageId += 1}`);
getStats({
job: job
}, (err, stats) => {
if (err) {
return console.error(err);
}
server.publish(`/stats/${job}`, {
when: new Date().getTime(),
stats
});
});
};
cdm[job] = {
interval: setInterval(update, 1000),
sockets: 1
};
},
off: (job) => {
console.log('off', job);
if (!(cdm[job].sockets -= 1)) {
clearInterval(cdm[job].interval);
}
}
});
module.exports.tree = (ctx) => {
return prometheus.tree({
query: ['node_memory_heap_used_bytes']
});
};

View File

@ -77,7 +77,6 @@
"react/prefer-stateless-function": 2,
"react/self-closing-comp": 2,
"react/sort-comp": 2,
"react/sort-prop-types": 2,
"react/style-prop-object": 2,
"react/jsx-boolean-value": [2, "never"],
"react/jsx-closing-bracket-location": 2,