Add /paths page that displays metrics by path, add ability to sort Table (#133)

* Add /paths page that shows rollup metrics by path
* Clean up ApiHelpers a bit

Adds ability to sort by column in the tabbed metrics table (to make a TabbedMetricsTable sortable, set sortable={true})

Adds a page, accessible via /paths that shows a table of all paths, with their request/success/latency metrics. I haven't exposed it in the sidebar as it doesn't have design treatment.
This commit is contained in:
Risha Mars 2018-01-11 17:36:48 -08:00 committed by GitHub
parent 63d1a5d70d
commit 1cf9da8ee7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 244 additions and 76 deletions

View File

@ -220,6 +220,10 @@ a.button.primary:active {
border-bottom: 1px solid;
border-color: var(--status-gray);
word-break: keep-all;
&.ant-table-column-sort {
background: none;
}
}
& .ant-table table {

View File

@ -49,10 +49,11 @@ export default class Deployment extends React.Component {
pollingInterval: 10000,
metricsWindow: "10m",
deploy: deployment,
metrics:[],
metrics: [],
pods: [],
upstreamMetrics: [],
downstreamMetrics: [],
pathMetrics: [],
pendingRequests: false,
loaded: false,
error: ''
@ -71,20 +72,23 @@ export default class Deployment extends React.Component {
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 pathMetricsUrl = urls["path"].url(this.state.deploy).rollup;
let deployFetch = this.api.fetch(deployMetricsUrl);
let podListFetch = this.api.fetchPods();
let podRollupFetch = this.api.fetch(podRollupUrl);
let upstreamFetch = this.api.fetch(upstreamRollupUrl);
let downstreamFetch = this.api.fetch(downstreamRollupUrl);
let pathsFetch = this.api.fetch(pathMetricsUrl);
// expose serverPromise for testing
this.serverPromise = Promise.all([deployFetch, podRollupFetch, upstreamFetch, downstreamFetch, podListFetch])
.then(([deployMetrics, podRollup, upstreamRollup, downstreamRollup, podList]) => {
this.serverPromise = Promise.all([deployFetch, podRollupFetch, upstreamFetch, downstreamFetch, podListFetch, pathsFetch])
.then(([deployMetrics, podRollup, upstreamRollup, downstreamRollup, podList, paths]) => {
let tsByDeploy = processTimeseriesMetrics(deployMetrics.metrics, "targetDeploy");
let podMetrics = processRollupMetrics(podRollup.metrics, "targetPod");
let upstreamMetrics = processRollupMetrics(upstreamRollup.metrics, "sourceDeploy");
let downstreamMetrics = processRollupMetrics(downstreamRollup.metrics, "targetDeploy");
let pathMetrics = processRollupMetrics(paths.metrics, "path");
let deploy = _.find(getPodsByDeployment(podList.pods), ["name", this.state.deploy]);
let totalRequestRate = _.sumBy(podMetrics, "requestRate");
@ -97,6 +101,7 @@ export default class Deployment extends React.Component {
deployTs: _.get(tsByDeploy, this.state.deploy, {}),
upstreamMetrics: upstreamMetrics,
downstreamMetrics: downstreamMetrics,
pathMetrics: pathMetrics,
lastUpdated: Date.now(),
pendingRequests: false,
loaded: true,
@ -147,7 +152,8 @@ export default class Deployment extends React.Component {
upstreamMetrics={this.state.upstreamMetrics}
downstreamMetrics={this.state.downstreamMetrics}
metricsWindow={this.state.metricsWindow}
pathPrefix={this.props.pathPrefix} />
pathPrefix={this.props.pathPrefix} />,
this.renderPaths()
];
}
@ -201,6 +207,24 @@ export default class Deployment extends React.Component {
);
}
renderPaths() {
return _.size(this.state.pathMetrics) === 0 ? null :
<div key="deployment-paths">
<div className="border-container border-neutral subsection-header">
<div className="border-container-content subsection-header">
Paths
</div>
</div>
<TabbedMetricsTable
resource="path"
metrics={this.state.pathMetrics}
hideSparklines={true}
lastUpdated={this.props.lastUpdated}
metricsWindow={this.props.metricsWindow}
pathPrefix={this.props.pathPrefix} />
</div>;
}
renderDeploymentTitle() {
return (
<div className="deployment-title">

View File

@ -0,0 +1,90 @@
import ConduitSpinner from "./ConduitSpinner.jsx";
import ErrorBanner from './ErrorBanner.jsx';
import { processRollupMetrics } from './util/MetricUtils.js';
import React from 'react';
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
import { ApiHelpers, urlsForResource } from './util/ApiHelpers.js';
import 'whatwg-fetch';
export default class Paths 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);
this.state = this.initialState();
}
componentDidMount() {
this.loadFromServer();
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
}
componentWillUnmount() {
window.clearInterval(this.timerId);
}
initialState() {
return {
lastUpdated: 0,
pollingInterval: 10000,
metricsWindow: "1m",
metrics: [],
pendingRequests: false,
loaded: false,
error: ''
};
}
loadFromServer() {
if (this.state.pendingRequests) {
return;
}
this.setState({ pendingRequests: true });
let urls = urlsForResource(this.props.pathPrefix, this.state.metricsWindow);
this.api.fetch(urls["path"].url().rollup).then(r => {
let metrics = processRollupMetrics(r.metrics, "path");
this.setState({
metrics: metrics,
lastUpdated: Date.now(),
pendingRequests: false,
loaded: true,
error: ''
});
}).catch(this.handleApiError);
}
handleApiError(e) {
this.setState({
pendingRequests: false,
error: `Error getting data from server: ${e.message}`
});
}
render() {
return (
<div className="page-content">
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
{ !this.state.loaded ? <ConduitSpinner /> :
<div>
<div className="page-header">
<h1>Paths</h1>
</div>
<TabbedMetricsTable
resource="path"
metrics={this.state.metrics}
lastUpdated={this.state.lastUpdated}
pathPrefix={this.props.pathPrefix}
metricsWindow={this.state.metricsWindow}
sortable={true}
hideSparklines={true} />
</div>
}
</div>
);
}
}

View File

@ -19,74 +19,91 @@ const resourceInfo = {
"upstream_pod": { title: "upstream pod", url: "/pod?pod=" },
"downstream_pod": { title: "downstream pod", url: "/pod?pod=" },
"deployment": { title: "deployment", url: "/deployment?deploy=" },
"pod": { title: "pod", url: "/pod?pod=" }
"pod": { title: "pod", url: "/pod?pod=" },
"path": { title: "path", url: null }
};
const columns = {
resourceName: (resource, pathPrefix) => {
return {
title: resource.title,
dataIndex: "name",
key: "name",
render: name => <Link to={`${pathPrefix}${resource.url}${name}`}>{name}</Link>
};
},
successRate: {
title: "Success Rate",
dataIndex: "successRate",
key: "successRateRollup",
className: "numeric",
render: d => metricToFormatter["SUCCESS_RATE"](d)
},
requests: {
title: "Request Rate",
dataIndex: "requestRate",
key: "requestRateRollup",
className: "numeric",
render: d => metricToFormatter["REQUEST_RATE"](d)
},
requestDistribution: {
title: "Request distribution",
key: "distribution",
className: "numeric",
render: d => (new Percentage(d.requestRate, d.totalRequests)).prettyRate()
},
latencyP99: {
title: "P99 Latency",
dataIndex: "latency",
key: "p99LatencyRollup",
className: "numeric",
render: d => metricToFormatter["LATENCY"](_.get(d, ["P99", 0, "value"]))
},
latencyP95: {
title: "P95 Latency",
dataIndex: "latency",
key: "p95LatencyRollup",
className: "numeric",
render: d => metricToFormatter["LATENCY"](_.get(d, ["P95", 0, "value"]))
},
latencyP50: {
title: "P50 Latency",
dataIndex: "latency",
key: "p50LatencyRollup",
className: "numeric",
render: d => metricToFormatter["LATENCY"](_.get(d, ["P50", 0, "value"]))
}
const generateColumns = sortable => {
return {
resourceName: (resource, pathPrefix) => {
return {
title: resource.title,
dataIndex: "name",
key: "name",
sorter: sortable ? (a, b) => (a.name || "").localeCompare(b.name) : false,
render: name => !resource.url ? name :
<Link to={`${pathPrefix}${resource.url}${name}`}>{name}</Link>
};
},
successRate: {
title: "Success Rate",
dataIndex: "successRate",
key: "successRateRollup",
className: "numeric",
sorter: sortable ? (a, b) => numericSort(a.successRate, b.successRate) : false,
render: d => metricToFormatter["SUCCESS_RATE"](d)
},
requests: {
title: "Request Rate",
dataIndex: "requestRate",
key: "requestRateRollup",
className: "numeric",
sorter: sortable ? (a, b) => numericSort(a.requestRate, b.requestRate) : false,
render: d => metricToFormatter["REQUEST_RATE"](d)
},
requestDistribution: {
title: "Request distribution",
dataIndex: "requestDistribution",
key: "distribution",
className: "numeric",
sorter: sortable ? (a, b) =>
numericSort(a.requestDistribution.get(), b.requestDistribution.get()) : false,
render: d => d.prettyRate()
},
latencyP99: {
title: "P99 Latency",
dataIndex: "P99",
key: "p99LatencyRollup",
className: "numeric",
sorter: sortable ? (a, b) => numericSort(a.P99, b.P99) : false,
render: metricToFormatter["LATENCY"]
},
latencyP95: {
title: "P95 Latency",
dataIndex: "P95",
key: "p95LatencyRollup",
className: "numeric",
sorter: sortable ? (a, b) => numericSort(a.P95, b.P95) : false,
render: metricToFormatter["LATENCY"]
},
latencyP50: {
title: "P50 Latency",
dataIndex: "P50",
key: "p50LatencyRollup",
className: "numeric",
sorter: sortable ? (a, b) => numericSort(a.P50, b.P50) : false,
render: metricToFormatter["LATENCY"]
}
};
};
const metricToColumns = {
requestRate: (resource, pathPrefix) => [
columns.resourceName(resource, pathPrefix),
columns.requests,
resource.title === "deployment" ? null : columns.requestDistribution
],
successRate: (resource, pathPrefix) => [columns.resourceName(resource, pathPrefix), columns.successRate],
latency: (resource, pathPrefix) => [
columns.resourceName(resource, pathPrefix),
columns.latencyP99,
columns.latencyP95,
columns.latencyP50
]
const numericSort = (a, b) => (_.isNil(a) ? -1 : a) - (_.isNil(b) ? -1 : b);
const metricToColumns = baseCols => {
return {
requestRate: (resource, pathPrefix) => [
baseCols.resourceName(resource, pathPrefix),
baseCols.requests,
resource.title === "deployment" ? null : baseCols.requestDistribution
],
successRate: (resource, pathPrefix) => [baseCols.resourceName(resource, pathPrefix), baseCols.successRate],
latency: (resource, pathPrefix) => [
baseCols.resourceName(resource, pathPrefix),
baseCols.latencyP99,
baseCols.latencyP95,
baseCols.latencyP50
]
};
};
const nameToDataKey = {
@ -106,6 +123,7 @@ export default class TabbedMetricsTable extends React.Component {
this.state = {
timeseries: {},
rollup: this.preprocessMetrics(),
groupBy: tsHelper.groupBy,
metricsUrl: tsHelper.url(this.props.entity),
error: '',
@ -127,6 +145,25 @@ export default class TabbedMetricsTable extends React.Component {
window.clearInterval(this.timerId);
}
preprocessMetrics() {
let tableData = this.props.metrics;
let totalRequestRate = _.sumBy(this.props.metrics, "requestRate") || 0;
_.each(tableData, datum => {
datum.totalRequests = totalRequestRate;
datum.requestDistribution = new Percentage(datum.requestRate, datum.totalRequests);
_.each(datum.latency, (d, quantile) => {
_.each(d, datapoint => {
let latencyValue = _.isNil(datapoint.value) ? null : parseInt(datapoint.value, 10);
datum[quantile] = latencyValue;
});
});
});
return tableData;
}
loadFromServer() {
if (this.state.pendingRequests) {
return; // don't make more requests if the ones we sent haven't completed
@ -177,18 +214,14 @@ export default class TabbedMetricsTable extends React.Component {
renderTable(metric) {
let resource = resourceInfo[this.props.resource];
let columns = _.compact(metricToColumns[metric](resource, this.props.pathPrefix));
let columnDefinitions = metricToColumns(generateColumns(this.props.sortable));
let columns = _.compact(columnDefinitions[metric](resource, this.props.pathPrefix));
if (!this.props.hideSparklines) {
columns.push(this.getSparklineColumn(metric));
}
// TODO: move this into rollup aggregation
let tableData = this.props.metrics;
let totalRequestRate = _.sumBy(this.props.metrics, "requestRate") || 0;
_.each(tableData, datum => datum.totalRequests = totalRequestRate);
return (<Table
dataSource={tableData}
dataSource={this.state.rollup}
columns={columns}
pagination={false}
className="conduit-table"

View File

@ -36,11 +36,12 @@ export const urlsForResource = (pathPrefix, metricsWindow) => {
"deployment": {
groupBy: "targetDeploy",
url: (deploy = null) => {
let rollupUrl = !deploy ? metricsUrl : `${metricsUrl}&target_deploy=${deploy}`;
let timeseriesUrl = !deploy ? `${metricsUrl}&timeseries=true` :
`${metricsUrl}&timeseries=true&target_deploy=${deploy}`;
return {
ts: timeseriesUrl,
rollup: metricsUrl
rollup: rollupUrl
};
}
},
@ -103,6 +104,19 @@ export const urlsForResource = (pathPrefix, metricsWindow) => {
rollup: downstreamRollupUrl
};
}
},
"path": {
// all paths (default), or all paths of a given deploy if specified
groupBy: "path",
url: (deploy = null) => {
let pathRollupUrl = `${metricsUrl}&aggregation=path${ !deploy ? "" : `&target_deploy=${deploy}`}`;
let pathTsUrl = `${pathRollupUrl}&timeseries=true`;
return {
ts: pathTsUrl,
rollup: pathRollupUrl
};
}
}
};
};

View File

@ -1,6 +1,7 @@
import Deployment from './components/Deployment.jsx';
import Deployments from './components/Deployments.jsx';
import NoMatch from './components/NoMatch.jsx';
import Paths from './components/Paths.jsx';
import PodDetail from './components/PodDetail.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
@ -33,6 +34,7 @@ ReactDOM.render((
<Route path={`${pathPrefix}/servicemesh`} render={() => <ServiceMesh pathPrefix={pathPrefix} releaseVersion={appData.releaseVersion} />} />
<Route path={`${pathPrefix}/deployments`} render={() => <Deployments pathPrefix={pathPrefix} />} />
<Route path={`${pathPrefix}/deployment`} render={props => <Deployment pathPrefix={pathPrefix} location={props.location} />} />
<Route path={`${pathPrefix}/paths`} render={props => <Paths pathPrefix={pathPrefix} location={props.location} />} />
<Route path={`${pathPrefix}/pod`} render={props => <PodDetail pathPrefix={pathPrefix} location={props.location} />} />
<Route path={`${pathPrefix}/routes`} render={() => <Routes pathPrefix={pathPrefix} />} />
<Route component={NoMatch} />

View File

@ -92,6 +92,7 @@ func NewServer(addr, templateDir, staticDir, uuid, webpackDevServer string, relo
server.router.GET("/pod", handler.handleIndex)
server.router.GET("/deployment", handler.handleIndex)
server.router.GET("/deployments", handler.handleIndex)
server.router.GET("/paths", handler.handleIndex)
server.router.GET("/servicemesh", handler.handleIndex)
server.router.GET("/routes", handler.handleIndex)
server.router.ServeFiles(