Make TabbedMetricsTable in charge of fetching timeseries (#89)

* Make TabbedMetricsTable in charge of fetching timeseries

All of the parent components in charge of fetching timeseries data
don't actually use them, but pass them to this table. It would simplify
a lot of the parents if this component handled the ts fetching too.

This introduces a urlsForResource helper in ApiHelpers to allow
us to consolidate url generating in the app to one place. It also
associates the appropriate groupBy for various fetches.

Signed-off-by: Risha Mars <mars@buoyant.io>
This commit is contained in:
Risha Mars 2018-01-03 16:55:56 -08:00 committed by GitHub
parent a7306113d2
commit fa49de6ff4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 208 additions and 99 deletions

View File

@ -1,5 +1,4 @@
import _ from 'lodash';
import { ApiHelpers } from './util/ApiHelpers.js';
import BarChart from './BarChart.jsx';
import ConduitSpinner from "./ConduitSpinner.jsx";
import ErrorBanner from './ErrorBanner.jsx';
@ -11,6 +10,7 @@ import { rowGutter } from './util/Utils.js';
import StatPane from './StatPane.jsx';
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
import UpstreamDownstream from './UpstreamDownstream.jsx';
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
import { Col, Row } from 'antd';
import { emptyMetric, getPodsByDeployment, processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
import './../../css/deployment.css';
@ -50,12 +50,9 @@ export default class Deployment extends React.Component {
metricsWindow: "10m",
deploy: deployment,
metrics:[],
timeseriesByPod: {},
pods: [],
upstreamMetrics: [],
upstreamTsByDeploy: {},
downstreamMetrics: [],
downstreamTsByDeploy: {},
pendingRequests: false,
loaded: false,
error: ''
@ -68,35 +65,26 @@ export default class Deployment extends React.Component {
}
this.setState({ pendingRequests: true });
let metricsUrl = `${this.props.pathPrefix}/api/metrics?window=${this.state.metricsWindow}` ;
let deployMetricsUrl = `${metricsUrl}&timeseries=true&target_deploy=${this.state.deploy}`;
let podRollupUrl = `${metricsUrl}&aggregation=target_pod&target_deploy=${this.state.deploy}`;
let podTimeseriesUrl = `${podRollupUrl}&timeseries=true`;
let upstreamRollupUrl = `${metricsUrl}&aggregation=source_deploy&target_deploy=${this.state.deploy}`;
let upstreamTimeseriesUrl = `${upstreamRollupUrl}&timeseries=true`;
let downstreamRollupUrl = `${metricsUrl}&aggregation=target_deploy&source_deploy=${this.state.deploy}`;
let downstreamTimeseriesUrl = `${downstreamRollupUrl}&timeseries=true`;
let urls = urlsForResource(this.props.pathPrefix, this.state.metricsWindow);
let deployMetricsUrl = urls["deployment"].url(this.state.deploy).ts;
let podRollupUrl = urls["pod"].url(this.state.deploy).rollup;
let upstreamRollupUrl = urls["upstream_deployment"].url(this.state.deploy).rollup;
let downstreamRollupUrl = urls["downstream_deployment"].url(this.state.deploy).rollup;
let deployFetch = this.api.fetch(deployMetricsUrl);
let podListFetch = this.api.fetchPods();
let podRollupFetch = this.api.fetch(podRollupUrl);
let podTsFetch = this.api.fetch(podTimeseriesUrl);
let upstreamFetch = this.api.fetch(upstreamRollupUrl);
let upstreamTsFetch = this.api.fetch(upstreamTimeseriesUrl);
let downstreamFetch = this.api.fetch(downstreamRollupUrl);
let downstreamTsFetch = this.api.fetch(downstreamTimeseriesUrl);
// expose serverPromise for testing
this.serverPromise = Promise.all([deployFetch, podRollupFetch, podTsFetch, upstreamFetch, upstreamTsFetch, downstreamFetch, downstreamTsFetch, podListFetch])
.then(([deployMetrics, podRollup, podTimeseries, upstreamRollup, upstreamTimeseries, downstreamRollup, downstreamTimeseries, podList]) => {
this.serverPromise = Promise.all([deployFetch, podRollupFetch, upstreamFetch, downstreamFetch, podListFetch])
.then(([deployMetrics, podRollup, upstreamRollup, downstreamRollup, podList]) => {
let tsByDeploy = processTimeseriesMetrics(deployMetrics.metrics, "targetDeploy");
let podMetrics = processRollupMetrics(podRollup.metrics, "targetPod");
let podTs = processTimeseriesMetrics(podTimeseries.metrics, "targetPod");
let upstreamMetrics = processRollupMetrics(upstreamRollup.metrics, "sourceDeploy");
let upstreamTsByDeploy = processTimeseriesMetrics(upstreamTimeseries.metrics, "sourceDeploy");
let downstreamMetrics = processRollupMetrics(downstreamRollup.metrics, "targetDeploy");
let downstreamTsByDeploy = processTimeseriesMetrics(downstreamTimeseries.metrics, "targetDeploy");
let deploy = _.find(getPodsByDeployment(podList.pods), ["name", this.state.deploy]);
let totalRequestRate = _.sumBy(podMetrics, "requestRate");
@ -104,14 +92,11 @@ export default class Deployment extends React.Component {
this.setState({
metrics: podMetrics,
timeseriesByPod: podTs,
pods: deploy.pods,
added: deploy.added,
deployTs: _.get(tsByDeploy, this.state.deploy, {}),
upstreamMetrics: upstreamMetrics,
upstreamTsByDeploy: upstreamTsByDeploy,
downstreamMetrics: downstreamMetrics,
downstreamTsByDeploy: downstreamTsByDeploy,
lastUpdated: Date.now(),
pendingRequests: false,
loaded: true,
@ -156,12 +141,12 @@ export default class Deployment extends React.Component {
this.renderMidsection(),
<UpstreamDownstream
key="deploy-upstream-downstream"
entity="deployment"
resource="deployment"
entity={this.state.deploy}
lastUpdated={this.state.lastUpdated}
upstreamMetrics={this.state.upstreamMetrics}
upstreamTsByEntity={this.state.upstreamTsByDeploy}
downstreamMetrics={this.state.downstreamMetrics}
downstreamTsByEntity={this.state.downstreamTsByDeploy}
metricsWindow={this.state.metricsWindow}
pathPrefix={this.props.pathPrefix} />
];
}
@ -194,10 +179,11 @@ export default class Deployment extends React.Component {
}
<TabbedMetricsTable
resource="pod"
lastUpdated={this.state.lastUpdated}
entity={this.state.deploy}
metrics={podTableData}
timeseries={this.state.timeseriesByPod}
pathPrefix={this.props.pathPrefix} />
lastUpdated={this.state.lastUpdated}
pathPrefix={this.props.pathPrefix}
metricsWindow={this.state.metricsWindow} />
</div>
</Col>

View File

@ -1,5 +1,4 @@
import _ from 'lodash';
import { ApiHelpers } from './util/ApiHelpers.js';
import CallToAction from './CallToAction.jsx';
import ConduitSpinner from "./ConduitSpinner.jsx";
import DeploymentSummary from './DeploymentSummary.jsx';
@ -7,6 +6,7 @@ import ErrorBanner from './ErrorBanner.jsx';
import React from 'react';
import { rowGutter } from './util/Utils.js';
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
import { Col, Row } from 'antd';
import { emptyMetric, getPodsByDeployment, processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
import './../../css/deployments.css';
@ -78,49 +78,34 @@ export default class Deployments extends React.Component {
}
loadTimeseriesFromServer(meshDeployMetrics, combinedMetrics) {
// fetch only the timeseries for the 3 deployments we display at the top of the page
let limitSparklineData = _.size(meshDeployMetrics) > maxTsToFetch;
let rollupPath = `${this.props.pathPrefix}/api/metrics?window=${this.state.metricsWindow}`;
let timeseriesPath = `${rollupPath}&timeseries=true`;
let updatedState = {
metrics: combinedMetrics,
limitSparklineData: limitSparklineData,
loaded: true,
pendingRequests: false,
error: ''
};
let resourceInfo = urlsForResource(this.props.pathPrefix, this.state.metricsWindow)["deployment"];
let leastHealthyDeployments = this.getLeastHealthyDeployments(meshDeployMetrics);
if (limitSparklineData) {
// don't fetch timeseries for every deploy
let leastHealthyDeployments = this.getLeastHealthyDeployments(meshDeployMetrics);
let tsPromises = _.map(leastHealthyDeployments, dep => {
let tsPathForDeploy = resourceInfo.url(dep.name).ts;
return this.api.fetch(tsPathForDeploy);
});
let tsPromises = _.map(leastHealthyDeployments, dep => {
let tsPathForDeploy = `${timeseriesPath}&target_deploy=${dep.name}`;
return this.api.fetch(tsPathForDeploy);
});
Promise.all(tsPromises)
.then(tsMetrics => {
let leastHealthyTs = _.reduce(tsMetrics, (mem, ea) => {
mem = mem.concat(ea.metrics);
return mem;
}, []);
let tsByDeploy = processTimeseriesMetrics(leastHealthyTs, "targetDeploy");
this.setState(_.merge({}, updatedState, {
timeseriesByDeploy: tsByDeploy,
lastUpdated: Date.now(),
}));
}).catch(this.handleApiError);
} else {
// fetch timeseries for all deploys
this.api.fetch(timeseriesPath)
.then(ts => {
let tsByDeploy = processTimeseriesMetrics(ts.metrics, "targetDeploy");
this.setState(_.merge({}, updatedState, {
timeseriesByDeploy: tsByDeploy,
lastUpdated: Date.now()
}));
}).catch(this.handleApiError);
}
Promise.all(tsPromises)
.then(tsMetrics => {
let leastHealthyTs = _.reduce(tsMetrics, (mem, ea) => {
mem = mem.concat(ea.metrics);
return mem;
}, []);
let tsByDeploy = processTimeseriesMetrics(leastHealthyTs, resourceInfo.groupBy);
this.setState({
timeseriesByDeploy: tsByDeploy,
lastUpdated: Date.now(),
metrics: combinedMetrics,
limitSparklineData: limitSparklineData,
loaded: true,
pendingRequests: false,
error: ''
});
}).catch(this.handleApiError);
}
handleApiError(e) {
@ -164,8 +149,8 @@ export default class Deployments extends React.Component {
resource="deployment"
lastUpdated={this.state.lastUpdated}
metrics={this.state.metrics}
timeseries={this.state.timeseriesByDeploy}
hideSparklines={this.state.limitSparklineData}
metricsWindow={this.state.metricsWindow}
pathPrefix={this.props.pathPrefix} />
</div>
</div>

View File

@ -1,11 +1,11 @@
import _ from 'lodash';
import { ApiHelpers } from './util/ApiHelpers.js';
import ConduitSpinner from "./ConduitSpinner.jsx";
import ErrorBanner from './ErrorBanner.jsx';
import HealthPane from './HealthPane.jsx';
import React from 'react';
import StatPane from './StatPane.jsx';
import UpstreamDownstream from './UpstreamDownstream.jsx';
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
import { processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
import 'whatwg-fetch';
@ -43,9 +43,7 @@ export default class PodDetail extends React.Component {
metricsWindow: "10m",
pod: pod,
upstreamMetrics: [],
upstreamTsByPod: {},
downstreamMetrics: [],
downstreamTsByPod: {},
podTs: {},
pendingRequests: false,
loaded: false,
@ -59,38 +57,31 @@ export default class PodDetail extends React.Component {
}
this.setState({ pendingRequests: true });
let metricsUrl = `${this.props.pathPrefix}/api/metrics?window=${this.state.metricsWindow}` ;
let podMetricsUrl = `${metricsUrl}&timeseries=true&target_pod=${this.state.pod}`;
let urls = urlsForResource(this.props.pathPrefix, this.state.metricsWindow);
let upstreamRollupUrl = `${metricsUrl}&aggregation=source_pod&target_pod=${this.state.pod}`;
let upstreamTimeseriesUrl = `${upstreamRollupUrl}&timeseries=true`;
let downstreamRollupUrl = `${metricsUrl}&aggregation=target_pod&source_pod=${this.state.pod}`;
let downstreamTimeseriesUrl = `${downstreamRollupUrl}&timeseries=true`;
let metricsUrl = urls["deployment"].url().rollup;
let podMetricsUrl = `${metricsUrl}&timeseries=true&target_pod=${this.state.pod}`;
let upstreamRollupUrl = urls["upstream_pod"].url(this.state.pod).rollup;
let downstreamRollupUrl = urls["downstream_pod"].url(this.state.pod).rollup;
let podFetch = this.api.fetch(podMetricsUrl);
let upstreamFetch = this.api.fetch(upstreamRollupUrl);
let upstreamTsFetch = this.api.fetch(upstreamTimeseriesUrl);
let downstreamFetch = this.api.fetch(downstreamRollupUrl);
let downstreamTsFetch = this.api.fetch(downstreamTimeseriesUrl);
Promise.all([podFetch, upstreamFetch, upstreamTsFetch, downstreamFetch, downstreamTsFetch])
.then(([podMetrics, upstreamRollup, upstreamTimeseries, downstreamRollup, downstreamTimeseries]) => {
Promise.all([podFetch, upstreamFetch, downstreamFetch])
.then(([podMetrics, upstreamRollup, downstreamRollup]) => {
let podTs = processTimeseriesMetrics(podMetrics.metrics, "targetPod");
let podTimeseries = _.get(podTs, this.state.pod, {});
let upstreamMetrics = processRollupMetrics(upstreamRollup.metrics, "sourcePod");
let upstreamTsByPod = processTimeseriesMetrics(upstreamTimeseries.metrics, "sourcePod");
let downstreamMetrics = processRollupMetrics(downstreamRollup.metrics, "targetPod");
let downstreamTsByPod = processTimeseriesMetrics(downstreamTimeseries.metrics, "targetPod");
this.setState({
pendingRequests: false,
lastUpdated: Date.now(),
podTs: podTimeseries,
upstreamMetrics: upstreamMetrics,
upstreamTsByPod: upstreamTsByPod,
downstreamMetrics: downstreamMetrics,
downstreamTsByPod: downstreamTsByPod,
loaded: true,
error: ''
});
@ -122,12 +113,12 @@ export default class PodDetail extends React.Component {
timeseries={this.state.podTs} />,
<UpstreamDownstream
key="pod-upstream-downstream"
entity="pod"
resource="pod"
entity={this.state.pod}
lastUpdated={this.state.lastUpdated}
upstreamMetrics={this.state.upstreamMetrics}
upstreamTsByEntity={this.state.upstreamTsByPod}
downstreamMetrics={this.state.downstreamMetrics}
downstreamTsByEntity={this.state.downstreamTsByPod}
metricsWindow={this.state.metricsWindow}
pathPrefix={this.props.pathPrefix} />
];
}

View File

@ -2,7 +2,9 @@ import _ from 'lodash';
import LineGraph from './LineGraph.jsx';
import { Link } from 'react-router-dom';
import Percentage from './util/Percentage.js';
import { processTimeseriesMetrics } from './util/MetricUtils.js';
import React from 'react';
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
import { metricToFormatter, toClassName } from './util/Utils.js';
import { Table, Tabs } from 'antd';
@ -92,7 +94,64 @@ const nameToDataKey = {
successRate: "SUCCESS_RATE",
latency: "LATENCY"
};
export default class TabbedMetricsTable extends React.Component {
constructor(props) {
super(props);
this.api = ApiHelpers(this.props.pathPrefix);
this.handleApiError = this.handleApiError.bind(this);
this.loadFromServer = this.loadFromServer.bind(this);
let tsHelper = urlsForResource(this.props.pathPrefix, this.props.metricsWindow)[this.props.resource];
this.state = {
timeseries: {},
groupBy: tsHelper.groupBy,
metricsUrl: tsHelper.url(this.props.entity),
error: '',
lastUpdated: this.props.lastUpdated,
metricsWindow: "10s",
pollingInterval: 10000,
pendingRequests: false
};
}
componentDidMount() {
if (!this.props.hideSparklines) {
this.loadFromServer();
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
}
}
componentWillUnmount() {
window.clearInterval(this.timerId);
}
loadFromServer() {
if (this.state.pendingRequests) {
return; // don't make more requests if the ones we sent haven't completed
}
this.setState({ pendingRequests: true });
this.api.fetch(this.state.metricsUrl.ts)
.then(tsResp => {
let tsByEntity = processTimeseriesMetrics(tsResp.metrics, this.state.groupBy);
this.setState({
timeseries: tsByEntity,
pendingRequests: false,
error: ''
});
})
.catch(this.handleApiError);
}
handleApiError(e) {
this.setState({
pendingRequests: false,
error: `Error getting data from server: ${e.message}`
});
}
getSparklineColumn(metricName) {
return {
title: "10 minute history",
@ -101,9 +160,9 @@ export default class TabbedMetricsTable extends React.Component {
render: d => {
let tsData;
if (metricName === "latency") {
tsData = _.get(this.props.timeseries, [d.name, "LATENCY", "P99"], []);
tsData = _.get(this.state.timeseries, [d.name, "LATENCY", "P99"], []);
} else {
tsData = _.get(this.props.timeseries, [d.name, nameToDataKey[metricName]], []);
tsData = _.get(this.state.timeseries, [d.name, nameToDataKey[metricName]], []);
}
return (<LineGraph

View File

@ -4,6 +4,7 @@ import { rowGutter } from './util/Utils.js';
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
import { Col, Row } from 'antd';
const maxTsToFetch = 15;
export default class UpstreamDownstreamTables extends React.Component {
render() {
let numUpstreams = _.size(this.props.upstreamMetrics);
@ -16,14 +17,16 @@ export default class UpstreamDownstreamTables extends React.Component {
<div className="upstream-downstream-list">
<div className="border-container border-neutral subsection-header">
<div className="border-container-content subsection-header">
Upstream {this.props.entity}s: {numUpstreams}
Upstream {this.props.resource}s: {numUpstreams}
</div>
</div>
<TabbedMetricsTable
resource={`upstream_${this.props.entity}`}
resource={`upstream_${this.props.resource}`}
entity={this.props.entity}
hideSparklines={numUpstreams > maxTsToFetch}
lastUpdated={this.props.lastUpdated}
metrics={this.props.upstreamMetrics}
timeseries={this.props.upstreamTsByEntity}
metricsWindow={this.props.metricsWindow}
pathPrefix={this.props.pathPrefix} />
</div>
}
@ -32,14 +35,16 @@ export default class UpstreamDownstreamTables extends React.Component {
<div className="upstream-downstream-list">
<div className="border-container border-neutral subsection-header">
<div className="border-container-content subsection-header">
Downstream {this.props.entity}s: {numDownstreams}
Downstream {this.props.resource}s: {numDownstreams}
</div>
</div>
<TabbedMetricsTable
resource={`downstream_${this.props.entity}`}
resource={`downstream_${this.props.resource}`}
entity={this.props.entity}
hideSparklines={numDownstreams > maxTsToFetch}
lastUpdated={this.props.lastUpdated}
metrics={this.props.downstreamMetrics}
timeseries={this.props.downstreamTsByEntity}
metricsWindow={this.props.metricsWindow}
pathPrefix={this.props.pathPrefix} />
</div>
}

View File

@ -23,3 +23,86 @@ export const ApiHelpers = pathPrefix => {
fetchPods
};
};
export const urlsForResource = (pathPrefix, metricsWindow) => {
/*
Timeseries fetches used in the TabbedMetricsTable
Rollup fetches used throughout app
*/
let metricsUrl = `${pathPrefix}/api/metrics?window=${metricsWindow}`;
return {
// all deploys (default), or a given deploy if specified
"deployment": {
groupBy: "targetDeploy",
url: (deploy = null) => {
let timeseriesUrl = !deploy ? `${metricsUrl}&timeseries=true` :
`${metricsUrl}&timeseries=true&target_deploy=${deploy}`;
return {
ts: timeseriesUrl,
rollup: metricsUrl
};
}
},
"pod": {
// all pods of a given deploy
groupBy: "targetPod",
url: deploy => {
let podRollupUrl = `${metricsUrl}&aggregation=target_pod&target_deploy=${deploy}`;
let podTimeseriesUrl = `${podRollupUrl}&timeseries=true`;
return {
ts: podTimeseriesUrl,
rollup: podRollupUrl
};
}
},
"upstream_deployment": {
// all upstreams of a given deploy
groupBy: "sourceDeploy",
url: deploy => {
let upstreamRollupUrl = `${metricsUrl}&aggregation=source_deploy&target_deploy=${deploy}`;
let upstreamTimeseriesUrl = `${upstreamRollupUrl}&timeseries=true`;
return {
ts: upstreamTimeseriesUrl,
rollup: upstreamRollupUrl
};
}
},
"downstream_deployment": {
// all downstreams of a given deploy
groupBy: "targetDeploy",
url: deploy => {
let downstreamRollupUrl = `${metricsUrl}&aggregation=target_deploy&source_deploy=${deploy}`;
let downstreamTimeseriesUrl = `${downstreamRollupUrl}&timeseries=true`;
return {
ts: downstreamTimeseriesUrl,
rollup: downstreamRollupUrl
};
}
},
"upstream_pod": {
groupBy: "sourcePod",
url: pod => {
let upstreamRollupUrl = `${metricsUrl}&aggregation=source_pod&target_pod=${pod}`;
let upstreamTimeseriesUrl = `${upstreamRollupUrl}&timeseries=true`;
return {
ts: upstreamTimeseriesUrl,
rollup: upstreamRollupUrl
};
}
},
"downstream_pod": {
groupBy: "targetPod",
url: pod => {
let downstreamRollupUrl = `${metricsUrl}&aggregation=target_pod&source_pod=${pod}`;
let downstreamTimeseriesUrl = `${downstreamRollupUrl}&timeseries=true`;
return {
ts: downstreamTimeseriesUrl,
rollup: downstreamRollupUrl
};
}
}
};
};