mirror of https://github.com/linkerd/linkerd2.git
Add a namespace column to the metrics tables (#854)
* Add a namespace column to the metrics tables, support long resource names * Add a test for GrafanaLink * Change the PodList.jsx component to not use the ListPods api
This commit is contained in:
parent
97bf4fcdf2
commit
4661aaf30d
|
@ -241,6 +241,19 @@ a.button.primary:active {
|
|||
padding-right: 0;
|
||||
}
|
||||
|
||||
& tr > th.long-header > span {
|
||||
display: inline-block;
|
||||
margin-right: 0;
|
||||
|
||||
& div {
|
||||
float: left;
|
||||
|
||||
&.ant-table-column-sorter {
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-table-tbody > tr:hover > td {
|
||||
background: rgba(47, 128, 237, .1);
|
||||
}
|
||||
|
|
|
@ -4,15 +4,13 @@ export default class GrafanaLink extends React.Component {
|
|||
render() {
|
||||
let resourceVariableName = this.props.resource.toLowerCase().replace(" ", "_");
|
||||
let dashboardName = this.props.resource.toLowerCase().replace(" ", "-");
|
||||
let ownerInfo = this.props.name.split("/");
|
||||
let namespace = ownerInfo[0];
|
||||
let name = ownerInfo[1];
|
||||
|
||||
return (
|
||||
<this.props.conduitLink
|
||||
to={`/dashboard/db/conduit-${dashboardName}?var-namespace=${namespace}&var-${resourceVariableName}=${name}`}
|
||||
to={`/dashboard/db/conduit-${dashboardName}?var-namespace=${this.props.namespace}&var-${resourceVariableName}=${this.props.name}`}
|
||||
deployment={"grafana"}
|
||||
targetBlank={true}>
|
||||
{this.props.name} <i className="fa fa-external-link" />
|
||||
{this.props.displayName || this.props.name} <i className="fa fa-external-link" />
|
||||
</this.props.conduitLink>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,54 +20,76 @@ const withTooltip = (d, metricName) => {
|
|||
);
|
||||
};
|
||||
|
||||
const narrowColumnWidth = 100;
|
||||
const formatLongTitles = title => {
|
||||
let words = title.split(" ");
|
||||
if (words.length === 2) {
|
||||
return (<div className="table-long-title">{words[0]}<br />{words[1]}</div>);
|
||||
} else {
|
||||
return words;
|
||||
}
|
||||
};
|
||||
const columnDefinitions = (sortable = true, resource, ConduitLink) => {
|
||||
return [
|
||||
{
|
||||
title: "Namespace",
|
||||
key: "namespace",
|
||||
dataIndex: "namespace",
|
||||
sorter: sortable ? (a, b) => (a.namespace || "").localeCompare(b.namespace) : false
|
||||
},
|
||||
{
|
||||
title: resource,
|
||||
key: "name",
|
||||
defaultSortOrder: 'ascend',
|
||||
width: 150,
|
||||
sorter: sortable ? (a, b) => (a.name || "").localeCompare(b.name) : false,
|
||||
render: row => row.added ?
|
||||
<GrafanaLink name={row.name} resource={resource} conduitLink={ConduitLink} /> : row.name
|
||||
render: row => row.added ? <GrafanaLink
|
||||
name={row.name}
|
||||
namespace={row.namespace}
|
||||
resource={resource}
|
||||
conduitLink={ConduitLink} /> : row.name
|
||||
},
|
||||
{
|
||||
title: "Success Rate",
|
||||
title: formatLongTitles("Success Rate"),
|
||||
dataIndex: "successRate",
|
||||
key: "successRateRollup",
|
||||
className: "numeric",
|
||||
className: "numeric long-header",
|
||||
width: narrowColumnWidth,
|
||||
sorter: sortable ? (a, b) => numericSort(a.successRate, b.successRate) : false,
|
||||
render: d => metricToFormatter["SUCCESS_RATE"](d)
|
||||
},
|
||||
{
|
||||
title: "Request Rate",
|
||||
title: formatLongTitles("Request Rate"),
|
||||
dataIndex: "requestRate",
|
||||
key: "requestRateRollup",
|
||||
className: "numeric",
|
||||
className: "numeric long-header",
|
||||
width: narrowColumnWidth,
|
||||
sorter: sortable ? (a, b) => numericSort(a.requestRate, b.requestRate) : false,
|
||||
render: d => withTooltip(d, "REQUEST_RATE")
|
||||
},
|
||||
{
|
||||
title: "P50 Latency",
|
||||
title: formatLongTitles("P50 Latency"),
|
||||
dataIndex: "P50",
|
||||
key: "p50LatencyRollup",
|
||||
className: "numeric",
|
||||
className: "numeric long-header",
|
||||
width: narrowColumnWidth,
|
||||
sorter: sortable ? (a, b) => numericSort(a.P50, b.P50) : false,
|
||||
render: metricToFormatter["LATENCY"]
|
||||
},
|
||||
{
|
||||
title: "P95 Latency",
|
||||
title: formatLongTitles("P95 Latency"),
|
||||
dataIndex: "P95",
|
||||
key: "p95LatencyRollup",
|
||||
className: "numeric",
|
||||
className: "numeric long-header",
|
||||
width: narrowColumnWidth,
|
||||
sorter: sortable ? (a, b) => numericSort(a.P95, b.P95) : false,
|
||||
render: metricToFormatter["LATENCY"]
|
||||
},
|
||||
{
|
||||
title: "P99 Latency",
|
||||
title: formatLongTitles("P99 Latency"),
|
||||
dataIndex: "P99",
|
||||
key: "p99LatencyRollup",
|
||||
className: "numeric",
|
||||
className: "numeric long-header",
|
||||
width: narrowColumnWidth,
|
||||
sorter: sortable ? (a, b) => numericSort(a.P99, b.P99) : false,
|
||||
render: metricToFormatter["LATENCY"]
|
||||
}
|
||||
|
|
|
@ -34,17 +34,6 @@ export default class PodsList extends React.Component {
|
|||
this.api.cancelCurrentRequests();
|
||||
}
|
||||
|
||||
filterPods(pods, metrics) {
|
||||
let podsByName = _.keyBy(pods, 'name');
|
||||
return _.compact(_.map(metrics, metric => {
|
||||
let pod = podsByName[metric.name];
|
||||
if (pod && !pod.controlPlane) {
|
||||
metric.added = pod.added;
|
||||
return metric;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadFromServer() {
|
||||
if (this.state.pendingRequests) {
|
||||
return; // don't make more requests if the ones we sent haven't completed
|
||||
|
@ -52,17 +41,15 @@ export default class PodsList extends React.Component {
|
|||
this.setState({ pendingRequests: true });
|
||||
|
||||
this.api.setCurrentRequests([
|
||||
this.api.fetchMetrics(this.api.urlsForResource["pod"].url().rollup),
|
||||
this.api.fetchPods()
|
||||
this.api.fetchMetrics(this.api.urlsForResource["pod"].url().rollup)
|
||||
]);
|
||||
|
||||
Promise.all(this.api.getCurrentPromises())
|
||||
.then(([rollup, pods]) => {
|
||||
let meshPods = processRollupMetrics(rollup);
|
||||
let combinedMetrics = this.filterPods(pods.pods, meshPods);
|
||||
.then(([rollup]) => {
|
||||
let processedMetrics = processRollupMetrics(rollup);
|
||||
|
||||
this.setState({
|
||||
metrics: combinedMetrics,
|
||||
metrics: processedMetrics,
|
||||
loaded: true,
|
||||
pendingRequests: false,
|
||||
error: ''
|
||||
|
|
|
@ -44,8 +44,16 @@ const columns = {
|
|||
return {
|
||||
title: "Deployment",
|
||||
key: "name",
|
||||
render: row => shouldLink && row.added ?
|
||||
<GrafanaLink name={row.name} resource="deployment" conduitLink={ConduitLink} /> : row.name
|
||||
render: row => {
|
||||
let ownerInfo = row.name.split("/");
|
||||
return shouldLink && row.added ?
|
||||
<GrafanaLink
|
||||
name={ownerInfo[1]}
|
||||
namespace={ownerInfo[0]}
|
||||
displayName={row.name}
|
||||
resource="deployment"
|
||||
conduitLink={ConduitLink} /> : row.name;
|
||||
}
|
||||
};
|
||||
},
|
||||
pods: {
|
||||
|
|
|
@ -107,7 +107,8 @@ export const processRollupMetrics = (rawMetrics, controllerNamespace) => {
|
|||
return null;
|
||||
}
|
||||
return {
|
||||
name: row.resource.namespace + "/" + row.resource.name,
|
||||
name: row.resource.name,
|
||||
namespace: row.resource.namespace,
|
||||
requestRate: getRequestRate(row),
|
||||
successRate: getSuccessRate(row),
|
||||
latency: getLatency(row),
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import { ApiHelpers } from '../js/components/util/ApiHelpers.jsx';
|
||||
import Enzyme from 'enzyme';
|
||||
import { expect } from 'chai';
|
||||
import GrafanaLink from '../js/components/GrafanaLink.jsx';
|
||||
import { mount } from 'enzyme';
|
||||
import { routerWrap } from './testHelpers.jsx';
|
||||
import sinon from 'sinon';
|
||||
import sinonStubPromise from 'sinon-stub-promise';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
sinonStubPromise(sinon);
|
||||
|
||||
describe('GrafanaLink', () => {
|
||||
it('makes a link', () => {
|
||||
let api = ApiHelpers('');
|
||||
let linkProps = {
|
||||
resource: "Replication Controller",
|
||||
name: "aldksf-3409823049823",
|
||||
namespace: "myns",
|
||||
conduitLink: api.ConduitLink
|
||||
};
|
||||
let component = mount(routerWrap(GrafanaLink, linkProps));
|
||||
|
||||
let expectedDashboardNameStr = "/conduit-replication-controller";
|
||||
let expectedNsStr = "var-namespace=myns";
|
||||
let expectedVarNameStr = "var-replication_controller=aldksf-3409823049823";
|
||||
|
||||
expect(component.find("GrafanaLink")).to.have.length(1);
|
||||
expect(component.html()).to.contain(expectedDashboardNameStr);
|
||||
expect(component.html()).to.contain(expectedNsStr);
|
||||
expect(component.html()).to.contain(expectedVarNameStr);
|
||||
});
|
||||
});
|
|
@ -9,7 +9,8 @@ describe('MetricUtils', () => {
|
|||
let result = processRollupMetrics(deployRollupFixtures);
|
||||
let expectedResult = [
|
||||
{
|
||||
name: 'emojivoto/voting',
|
||||
name: 'voting',
|
||||
namespace: 'emojivoto',
|
||||
requestRate: 2.5,
|
||||
successRate: 0.9,
|
||||
latency: {
|
||||
|
@ -26,10 +27,14 @@ describe('MetricUtils', () => {
|
|||
it('Extracts and sorts multiple deploys from a single response', () => {
|
||||
let result = processRollupMetrics(multiDeployRollupFixtures);
|
||||
expect(result).to.have.length(4);
|
||||
expect(result[0].name).to.equal("emojivoto/emoji");
|
||||
expect(result[1].name).to.equal("emojivoto/vote-bot");
|
||||
expect(result[2].name).to.equal("emojivoto/voting");
|
||||
expect(result[3].name).to.equal("emojivoto/web");
|
||||
expect(result[0].name).to.equal("emoji");
|
||||
expect(result[0].namespace).to.equal("emojivoto");
|
||||
expect(result[1].name).to.equal("vote-bot");
|
||||
expect(result[1].namespace).to.equal("emojivoto");
|
||||
expect(result[2].name).to.equal("voting");
|
||||
expect(result[2].namespace).to.equal("emojivoto");
|
||||
expect(result[3].name).to.equal("web");
|
||||
expect(result[3].namespace).to.equal("emojivoto");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue