mirror of https://github.com/linkerd/linkerd2.git
Add trafficsplit to dashboard (#3333)
Fixes #3261. Adds trafficsplit data to the dashboard via the Resources sidebar.
This commit is contained in:
parent
e281fb3410
commit
d4f3f210ce
|
|
@ -76,8 +76,43 @@ const httpStatColumns = [
|
|||
|
||||
];
|
||||
|
||||
const columnDefinitions = (resource, showNamespaceColumn, PrefixedLink, isTcpTable) => {
|
||||
const trafficSplitDetailColumns = [
|
||||
{
|
||||
title: "Apex Service",
|
||||
dataIndex: "apex",
|
||||
isNumeric: false,
|
||||
filter: d => !d.tsStats ? null : d.tsStats.apex,
|
||||
render: d => !d.tsStats ? null : d.tsStats.apex,
|
||||
sorter: d => !d.tsStats ? null : d.tsStats.apex
|
||||
},
|
||||
{
|
||||
title: "Leaf Service",
|
||||
dataIndex: "leaf",
|
||||
isNumeric: false,
|
||||
filter: d => !d.tsStats ? null : d.tsStats.leaf,
|
||||
render: d => !d.tsStats ? null : d.tsStats.leaf,
|
||||
sorter: d => !d.tsStats ? null : d.tsStats.leaf
|
||||
},
|
||||
{
|
||||
title: "Weight",
|
||||
dataIndex: "weight",
|
||||
isNumeric: true,
|
||||
filter: d => !d.tsStats ? null : d.tsStats.weight,
|
||||
render: d => !d.tsStats ? null : d.tsStats.weight,
|
||||
sorter: d => {
|
||||
if (!d.tsStats) {return;}
|
||||
if (parseInt(d.tsStats.weight)) {
|
||||
return parseInt(d.tsStats.weight);
|
||||
} else {
|
||||
return d.tsStats.weight;
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, PrefixedLink, isTcpTable) => {
|
||||
let isAuthorityTable = resource === "authority";
|
||||
let isTrafficSplitTable = resource === "trafficsplit";
|
||||
let isMultiResourceTable = resource === "multi_resource";
|
||||
let getResourceDisplayName = isMultiResourceTable ? displayName : d => d.name;
|
||||
|
||||
|
|
@ -128,7 +163,7 @@ const columnDefinitions = (resource, showNamespaceColumn, PrefixedLink, isTcpTab
|
|||
let nameContents;
|
||||
if (resource === "namespace") {
|
||||
nameContents = <PrefixedLink to={"/namespaces/" + d.name}>{d.name}</PrefixedLink>;
|
||||
} else if (!d.added || isAuthorityTable) {
|
||||
} else if (!d.added && (!isTrafficSplitTable || isAuthorityTable)) {
|
||||
nameContents = getResourceDisplayName(d);
|
||||
} else {
|
||||
nameContents = (
|
||||
|
|
@ -148,20 +183,27 @@ const columnDefinitions = (resource, showNamespaceColumn, PrefixedLink, isTcpTab
|
|||
sorter: d => getResourceDisplayName(d) || -1
|
||||
};
|
||||
|
||||
let columns = [nameColumn];
|
||||
let columns = [];
|
||||
if (showNameColumn) {
|
||||
columns = [nameColumn];
|
||||
}
|
||||
if (isTrafficSplitTable) {
|
||||
columns = columns.concat(trafficSplitDetailColumns);
|
||||
}
|
||||
if (isTcpTable) {
|
||||
columns = columns.concat(tcpStatColumns);
|
||||
} else {
|
||||
columns = columns.concat(httpStatColumns);
|
||||
}
|
||||
columns = columns.concat(grafanaColumn);
|
||||
|
||||
|
||||
// don't add the meshed column on a Authority MetricsTable
|
||||
if (!isAuthorityTable) {
|
||||
if (!isAuthorityTable && !isTrafficSplitTable) {
|
||||
columns.splice(1, 0, meshedColumn);
|
||||
}
|
||||
|
||||
if (!isTrafficSplitTable) {
|
||||
columns = columns.concat(grafanaColumn);
|
||||
}
|
||||
|
||||
if (!showNamespaceColumn) {
|
||||
return columns;
|
||||
} else {
|
||||
|
|
@ -190,23 +232,27 @@ class MetricsTable extends React.Component {
|
|||
isTcpTable: PropTypes.bool,
|
||||
metrics: PropTypes.arrayOf(processedMetricsPropType),
|
||||
resource: PropTypes.string.isRequired,
|
||||
showName: PropTypes.bool,
|
||||
showNamespaceColumn: PropTypes.bool,
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showNamespaceColumn: true,
|
||||
showName: true,
|
||||
title: "",
|
||||
isTcpTable: false,
|
||||
metrics: []
|
||||
};
|
||||
|
||||
render() {
|
||||
const { metrics, resource, showNamespaceColumn, title, api, isTcpTable } = this.props;
|
||||
const { metrics, resource, showNamespaceColumn, showName, title, api, isTcpTable } = this.props;
|
||||
|
||||
let showNsColumn = resource === "namespace" ? false : showNamespaceColumn;
|
||||
|
||||
let columns = columnDefinitions(resource, showNsColumn, api.PrefixedLink, isTcpTable);
|
||||
let showNameColumn = resource !== "trafficsplit" ? true : showName;
|
||||
let orderBy = "name";
|
||||
if (resource === "trafficsplit" && !showNameColumn) {orderBy = "leaf";}
|
||||
let columns = columnDefinitions(resource, showNsColumn, showNameColumn, api.PrefixedLink, isTcpTable);
|
||||
let rows = preprocessMetrics(metrics);
|
||||
return (
|
||||
<BaseTable
|
||||
|
|
@ -215,7 +261,7 @@ class MetricsTable extends React.Component {
|
|||
tableColumns={columns}
|
||||
tableClassName="metric-table"
|
||||
title={title}
|
||||
defaultOrderBy="name"
|
||||
defaultOrderBy={orderBy}
|
||||
padding="dense" />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,4 +98,14 @@ describe('Tests for <MetricsTable>', () => {
|
|||
expect(table).toBeDefined();
|
||||
expect(table.props().tableColumns).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('adds apex, leaf and weight columns, and omits meshed and grafana column, for a trafficsplit resource', () => {
|
||||
let extraProps = _merge({}, defaultProps, { metrics: [], resource: "trafficsplit"});
|
||||
const component = mount(routerWrap(MetricsTable, extraProps));
|
||||
|
||||
const table = component.find("BaseTable");
|
||||
|
||||
expect(table).toBeDefined();
|
||||
expect(table.props().tableColumns).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class Namespaces extends React.Component {
|
|||
|
||||
Promise.all(this.api.getCurrentPromises())
|
||||
.then(([allRollup]) => {
|
||||
let metrics = processMultiResourceRollup(allRollup);
|
||||
let metrics = processMultiResourceRollup(allRollup, "all");
|
||||
|
||||
this.setState({
|
||||
metrics: metrics,
|
||||
|
|
@ -146,6 +146,7 @@ class Namespaces extends React.Component {
|
|||
{this.renderResourceSection("replicationcontroller", metrics.replicationcontroller)}
|
||||
{this.renderResourceSection("statefulset", metrics.statefulset)}
|
||||
{this.renderResourceSection("job", metrics.job)}
|
||||
{this.renderResourceSection("trafficsplit", metrics.trafficsplit)}
|
||||
|
||||
{
|
||||
noMetrics ? null :
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ import NavigateNextIcon from '@material-ui/icons/NavigateNext';
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import SuccessRateDot from "./util/SuccessRateDot.jsx";
|
||||
import _each from 'lodash/each';
|
||||
import _includes from 'lodash/includes';
|
||||
import _merge from 'lodash/merge';
|
||||
import _orderBy from 'lodash/orderBy';
|
||||
import { friendlyTitle } from "./util/Utils.js";
|
||||
import { getAggregatedTrafficSplitMetrics } from './TrafficSplitDetail.jsx';
|
||||
import { processedMetricsPropType } from './util/MetricUtils.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
|
@ -32,6 +35,33 @@ const styles = () => ({
|
|||
}
|
||||
});
|
||||
|
||||
// the Stat API returns a row for each leaf within a trafficsplit. we should
|
||||
// list each trafficsplit only once in the sidebar, and calculate the aggregated
|
||||
// success rate so that the success rate dot is the correct color.
|
||||
const getTrafficSplitResourceList = metrics => {
|
||||
let splitsByName = {},
|
||||
splitListToDisplay = [];
|
||||
_each(metrics, row => {
|
||||
if (!_includes(splitsByName, row.name)) {
|
||||
splitsByName[row.name] = [row];
|
||||
} else {
|
||||
splitsByName[row.name].push(row);
|
||||
}
|
||||
});
|
||||
_each(splitsByName, split => {
|
||||
let metrics = getAggregatedTrafficSplitMetrics(split);
|
||||
let name = split[0].name;
|
||||
let namespace = split[0].namespace;
|
||||
splitListToDisplay.push({
|
||||
name: name,
|
||||
type: "trafficsplit",
|
||||
successRate: metrics.successRate,
|
||||
namespace: namespace,
|
||||
menuName: `${namespace}/${name}`});
|
||||
});
|
||||
return _orderBy(splitListToDisplay, split => split.menuName);
|
||||
};
|
||||
|
||||
class NavigationResource extends React.Component {
|
||||
static defaultProps = {
|
||||
metrics: [],
|
||||
|
|
@ -78,16 +108,20 @@ class NavigationResource extends React.Component {
|
|||
}
|
||||
|
||||
subMenu() {
|
||||
const { api, classes, metrics } = this.props;
|
||||
|
||||
const resources = _orderBy(metrics
|
||||
.filter(m => m.pods.meshedPods !== "0")
|
||||
.map(m =>
|
||||
_merge(m, {
|
||||
menuName: this.props.type === "namespaces" ? m.name : `${m.namespace}/${m.name}`
|
||||
})
|
||||
), r => r.menuName);
|
||||
const { api, classes, metrics, type } = this.props;
|
||||
let resources;
|
||||
|
||||
if (type === "trafficsplits") {
|
||||
resources = getTrafficSplitResourceList(metrics);
|
||||
} else {
|
||||
resources = _orderBy(metrics
|
||||
.filter(m => m.pods.meshedPods !== "0")
|
||||
.map(m =>
|
||||
_merge(m, {
|
||||
menuName: this.props.type === "namespaces" ? m.name : `${m.namespace}/${m.name}`
|
||||
})
|
||||
), r => r.menuName);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuList dense component="div" disablePadding>
|
||||
|
|
|
|||
|
|
@ -58,10 +58,10 @@ class NavigationResourcesBase extends React.Component {
|
|||
let allMetrics = {};
|
||||
let nsMetrics = {};
|
||||
if (_has(data, '[0]')) {
|
||||
allMetrics = processMultiResourceRollup(data[0]);
|
||||
allMetrics = processMultiResourceRollup(data[0], "all");
|
||||
|
||||
if (_has(data, '[1]')) {
|
||||
nsMetrics = processMultiResourceRollup(data[1]);
|
||||
nsMetrics = processMultiResourceRollup(data[1], "all");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +74,7 @@ class NavigationResourcesBase extends React.Component {
|
|||
<NavigationResource type="pods" metrics={allMetrics.pod} />
|
||||
<NavigationResource type="replicationcontrollers" metrics={allMetrics.replicationcontroller} />
|
||||
<NavigationResource type="statefulsets" metrics={allMetrics.statefulset} />
|
||||
<NavigationResource type="trafficsplits" metrics={allMetrics.trafficsplit} />
|
||||
</MenuList>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const maxNumNeighbors = 6; // max number of neighbor nodes to show in the octopu
|
|||
|
||||
const styles = () => ({
|
||||
graphContainer: {
|
||||
overflowX: "scroll",
|
||||
overflowX: "auto",
|
||||
padding: "16px 0"
|
||||
},
|
||||
graph: {
|
||||
|
|
@ -93,7 +93,8 @@ class Octopus extends React.Component {
|
|||
}
|
||||
|
||||
linkedResourceTitle = (resource, display) => {
|
||||
return _isNil(resource.namespace) ? display :
|
||||
// trafficsplit leaf resources cannot be linked
|
||||
return _isNil(resource.namespace) || resource.isLeafService ? display :
|
||||
<this.props.api.ResourceLink
|
||||
resource={resource}
|
||||
linkText={display} />;
|
||||
|
|
@ -107,7 +108,9 @@ class Octopus extends React.Component {
|
|||
|
||||
// if the resource only has TCP stats, display those instead
|
||||
let showTcp = false;
|
||||
if (_isNil(resource.successRate) && _isNil(resource.requestRate)) {
|
||||
// trafficsplit leaf with zero traffic should still show HTTP stats
|
||||
if (_isNil(resource.successRate) && _isNil(resource.requestRate) &&
|
||||
resource.type !== "service") {
|
||||
showTcp = true;
|
||||
}
|
||||
|
||||
|
|
@ -134,6 +137,12 @@ class Octopus extends React.Component {
|
|||
renderHttpStats(resource) {
|
||||
return (
|
||||
<TableBody>
|
||||
{resource.isLeafService &&
|
||||
<TableRow>
|
||||
<TableCell><Typography>Weight</Typography></TableCell>
|
||||
<TableCell numeric={true}><Typography>{resource.tsStats.weight}</Typography></TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
<TableRow>
|
||||
<TableCell><Typography>SR</Typography></TableCell>
|
||||
<TableCell numeric={true}><Typography>{metricToFormatter["SUCCESS_RATE"](resource.successRate)}</Typography></TableCell>
|
||||
|
|
@ -142,10 +151,12 @@ class Octopus extends React.Component {
|
|||
<TableCell><Typography>RPS</Typography></TableCell>
|
||||
<TableCell numeric={true}><Typography>{metricToFormatter["NO_UNIT"](resource.requestRate)}</Typography></TableCell>
|
||||
</TableRow>
|
||||
{!resource.isApexService &&
|
||||
<TableRow>
|
||||
<TableCell><Typography>P99</Typography></TableCell>
|
||||
<TableCell numeric={true}><Typography>{metricToFormatter["LATENCY"](_get(resource, "latency.P99"))}</Typography></TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
</TableBody>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import React from 'react';
|
|||
import SimpleChip from './util/Chip.jsx';
|
||||
import Spinner from './util/Spinner.jsx';
|
||||
import TopRoutesTabs from './TopRoutesTabs.jsx';
|
||||
import TrafficSplitDetail from './TrafficSplitDetail.jsx';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _filter from 'lodash/filter';
|
||||
import _get from 'lodash/get';
|
||||
|
|
@ -161,10 +162,10 @@ export class ResourceDetailBase extends React.Component {
|
|||
|
||||
Promise.all(this.api.getCurrentPromises())
|
||||
.then(([resourceRsp, podListRsp, podMetricsRsp, upstreamRsp, downstreamRsp, edgesRsp]) => {
|
||||
let resourceMetrics = processSingleResourceRollup(resourceRsp);
|
||||
let podMetrics = processSingleResourceRollup(podMetricsRsp);
|
||||
let upstreamMetrics = processMultiResourceRollup(upstreamRsp);
|
||||
let downstreamMetrics = processMultiResourceRollup(downstreamRsp);
|
||||
let resourceMetrics = processSingleResourceRollup(resourceRsp, resource.type);
|
||||
let podMetrics = processSingleResourceRollup(podMetricsRsp, resource.type);
|
||||
let upstreamMetrics = processMultiResourceRollup(upstreamRsp, resource.type);
|
||||
let downstreamMetrics = processMultiResourceRollup(downstreamRsp, resource.type);
|
||||
let edges = processEdges(edgesRsp, this.state.resource.name);
|
||||
|
||||
// INEFFICIENT: get metrics for all the pods belonging to this resource.
|
||||
|
|
@ -210,6 +211,7 @@ export class ResourceDetailBase extends React.Component {
|
|||
}
|
||||
|
||||
let isTcpOnly = !hasHttp && hasTcp;
|
||||
let isTrafficSplit = resource.type === "trafficsplit";
|
||||
|
||||
// figure out when the last traffic this resource received was so we can show a no traffic message
|
||||
let lastMetricReceivedTime = this.state.lastMetricReceivedTime;
|
||||
|
|
@ -220,12 +222,14 @@ export class ResourceDetailBase extends React.Component {
|
|||
this.setState({
|
||||
resourceMetrics,
|
||||
resourceIsMeshed,
|
||||
resourceRsp,
|
||||
podMetrics: podMetricsForResource,
|
||||
upstreamMetrics,
|
||||
downstreamMetrics,
|
||||
edges,
|
||||
lastMetricReceivedTime,
|
||||
isTcpOnly,
|
||||
isTrafficSplit,
|
||||
loaded: true,
|
||||
pendingRequests: false,
|
||||
error: null,
|
||||
|
|
@ -267,6 +271,7 @@ export class ResourceDetailBase extends React.Component {
|
|||
let {
|
||||
resourceName,
|
||||
resourceType,
|
||||
resourceRsp,
|
||||
namespace,
|
||||
resourceMetrics,
|
||||
edges,
|
||||
|
|
@ -274,6 +279,7 @@ export class ResourceDetailBase extends React.Component {
|
|||
resourceIsMeshed,
|
||||
lastMetricReceivedTime,
|
||||
isTcpOnly,
|
||||
isTrafficSplit,
|
||||
} = this.state;
|
||||
|
||||
let query = {
|
||||
|
|
@ -298,6 +304,15 @@ export class ResourceDetailBase extends React.Component {
|
|||
|
||||
let showNoTrafficMsg = resourceIsMeshed && (Date.now() - lastMetricReceivedTime > showNoTrafficMsgDelayMs);
|
||||
|
||||
if (isTrafficSplit) {
|
||||
return (
|
||||
<TrafficSplitDetail
|
||||
resourceType={resourceType}
|
||||
resourceName={resourceName}
|
||||
resourceMetrics={resourceMetrics}
|
||||
resourceRsp={resourceRsp} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Grid container justify="space-between" alignItems="center">
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class ResourceListBase extends React.Component {
|
|||
|
||||
let processedMetrics = [];
|
||||
if (data.length === 1) {
|
||||
processedMetrics = processSingleResourceRollup(data[0]);
|
||||
processedMetrics = processSingleResourceRollup(data[0], this.props.resource);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -51,11 +51,13 @@ export class ResourceListBase extends React.Component {
|
|||
metrics={processedMetrics}
|
||||
title="HTTP metrics" />
|
||||
|
||||
{this.props.resource !== "trafficsplit" &&
|
||||
<MetricsTable
|
||||
resource={this.props.resource}
|
||||
isTcpTable={true}
|
||||
metrics={processedMetrics}
|
||||
title="TCP metrics" />
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import Grid from '@material-ui/core/Grid';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import Octopus from './Octopus.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _each from 'lodash/each';
|
||||
import _isNil from 'lodash/isNil';
|
||||
import _reduce from 'lodash/reduce';
|
||||
import { processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
|
||||
// calculates the aggregated successRate and RPS for an entire trafficsplit
|
||||
export const getAggregatedTrafficSplitMetrics = resourceMetrics => {
|
||||
let totalRPS = 0;
|
||||
let successRates = [];
|
||||
_each(resourceMetrics, row => {
|
||||
if (!_isNil(row.requestRate)) {
|
||||
totalRPS+= row.requestRate;
|
||||
}
|
||||
if (!_isNil(row.successRate)) {
|
||||
let weightedSuccessRate = row.successRate * row.requestRate;
|
||||
successRates.push(weightedSuccessRate);
|
||||
}
|
||||
});
|
||||
let sumSuccessRates = _reduce(successRates, (acc, n) => {
|
||||
return acc+= n;
|
||||
}, 0);
|
||||
let aggregatedSuccessRate = sumSuccessRates/totalRPS || 0;
|
||||
return {successRate: aggregatedSuccessRate,
|
||||
totalRPS: totalRPS};
|
||||
};
|
||||
|
||||
const generateApexMetrics = resourceMetrics => {
|
||||
let aggregatedMetrics = getAggregatedTrafficSplitMetrics(resourceMetrics);
|
||||
return {
|
||||
name: resourceMetrics[0].tsStats.apex,
|
||||
type: "service",
|
||||
requestRate: aggregatedMetrics.totalRPS,
|
||||
successRate: aggregatedMetrics.successRate,
|
||||
isApexService: true,
|
||||
};
|
||||
};
|
||||
|
||||
// the Stat API returns a row for each leaf; however, to be consistent with
|
||||
// other resources, row.name is the trafficsplit name while row.tsStats.leaf is
|
||||
// the leaf name. here we replace the trafficsplit name with the leaf name for
|
||||
// the octopus graph.
|
||||
const formatLeaves = resourceRsp => {
|
||||
let leaves = processSingleResourceRollup(resourceRsp, "trafficsplit");
|
||||
_each(leaves, leaf => {
|
||||
leaf.name = leaf.tsStats.leaf;
|
||||
leaf.type = "service";
|
||||
leaf.isLeafService = true;
|
||||
});
|
||||
return leaves;
|
||||
};
|
||||
export default class TrafficSplitDetail extends React.Component {
|
||||
static propTypes = {
|
||||
resourceMetrics: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
resourceName: PropTypes.string.isRequired,
|
||||
resourceRsp: PropTypes.shape({}).isRequired,
|
||||
resourceType: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { resourceMetrics, resourceName, resourceRsp, resourceType } = this.props;
|
||||
const apexResource = generateApexMetrics(resourceMetrics);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container justify="space-between" alignItems="center">
|
||||
<Grid item><Typography variant="h5">{resourceType}/{resourceName}</Typography></Grid>
|
||||
</Grid>
|
||||
|
||||
<Octopus
|
||||
resource={apexResource}
|
||||
neighbors={{ upstream: [], downstream: formatLeaves(resourceRsp) }} />
|
||||
|
||||
<MetricsTable
|
||||
resource="trafficsplit"
|
||||
metrics={resourceMetrics}
|
||||
showName={false}
|
||||
showNamespaceColumn={false}
|
||||
title="Leaf Services" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -53,4 +53,3 @@ export const processedEdgesPropType = PropTypes.shape({
|
|||
type: PropTypes.string
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -112,8 +112,12 @@ const processStatTable = table => {
|
|||
let rows = _compact(table.podGroup.rows.map(row => {
|
||||
let runningPodCount = parseInt(row.runningPodCount, 10);
|
||||
let meshedPodCount = parseInt(row.meshedPodCount, 10);
|
||||
let rowKey = `${row.resource.namespace}-${row.resource.type}-${row.resource.name}`;
|
||||
if (row.tsStats) {
|
||||
rowKey = `${row.resource.namespace}-${row.resource.type}-${row.resource.name}-${row.tsStats.leaf}`;
|
||||
}
|
||||
return {
|
||||
key: `${row.resource.namespace}-${row.resource.type}-${row.resource.name}`,
|
||||
key: rowKey,
|
||||
name: row.resource.name,
|
||||
namespace: row.resource.namespace,
|
||||
type: row.resource.type,
|
||||
|
|
@ -122,6 +126,7 @@ const processStatTable = table => {
|
|||
successRate: getSuccessRate(row),
|
||||
latency: getLatency(row),
|
||||
tcp: getTcpStats(row),
|
||||
tsStats: row.tsStats,
|
||||
added: runningPodCount > 0 && meshedPodCount > 0,
|
||||
pods: {
|
||||
totalPods: row.runningPodCount,
|
||||
|
|
@ -149,8 +154,8 @@ export const processTopRoutesResults = rows => {
|
|||
));
|
||||
};
|
||||
|
||||
export const processSingleResourceRollup = rawMetrics => {
|
||||
let result = processMultiResourceRollup(rawMetrics);
|
||||
export const processSingleResourceRollup = (rawMetrics, resourceType) => {
|
||||
let result = processMultiResourceRollup(rawMetrics, resourceType);
|
||||
if (_size(result) > 1) {
|
||||
console.error("Multi metric returned; expected single metric.");
|
||||
}
|
||||
|
|
@ -160,7 +165,7 @@ export const processSingleResourceRollup = rawMetrics => {
|
|||
return _values(result)[0];
|
||||
};
|
||||
|
||||
export const processMultiResourceRollup = rawMetrics => {
|
||||
export const processMultiResourceRollup = (rawMetrics, resourceType) => {
|
||||
if (_isEmpty(rawMetrics.ok) || _isEmpty(rawMetrics.ok.statTables)) {
|
||||
return {};
|
||||
}
|
||||
|
|
@ -171,6 +176,18 @@ export const processMultiResourceRollup = rawMetrics => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Because trafficsplit metrics are now being returned from the Stat API
|
||||
// endpoint, `processMultiResourceRollup` can erroneously
|
||||
// return trafficsplit leaf services as "upstream" or "downstream" of a
|
||||
// selected resource in the Octopus graph.
|
||||
|
||||
// The below line checks if the statTable contains trafficsplit data and
|
||||
// returns early if the selected resource is not "all" or "trafficsplit",
|
||||
// since any other resource should not include trafficsplit metrics.
|
||||
if (table.podGroup.rows[0].tsStats && resourceType !== "all" && resourceType !== "trafficsplit") {
|
||||
return;
|
||||
}
|
||||
|
||||
// assumes all rows in a podGroup have the same resource type
|
||||
let resource = _get(table, ["podGroup", "rows", 0, "resource", "type"]);
|
||||
|
||||
|
|
|
|||
|
|
@ -127,6 +127,8 @@ export const friendlyTitle = singularOrPluralResource => {
|
|||
titleCase = _startCase("daemon set");
|
||||
} else if (resource === "statefulset") {
|
||||
titleCase = _startCase("stateful set");
|
||||
} else if (resource === "trafficsplit") {
|
||||
titleCase = _startCase("traffic split");
|
||||
}
|
||||
|
||||
let titles = { singular: titleCase };
|
||||
|
|
@ -155,6 +157,7 @@ const camelCaseLookUp = {
|
|||
"replicaset": "replicaSet",
|
||||
"replicationcontroller": "replicationController",
|
||||
"statefulset": "statefulSet",
|
||||
"trafficsplit": "trafficSplit",
|
||||
"daemonset": "daemonSet"
|
||||
};
|
||||
|
||||
|
|
@ -172,6 +175,7 @@ export const shortNameLookup = {
|
|||
"replicaset": "rs",
|
||||
"service": "svc",
|
||||
"statefulset": "sts",
|
||||
"trafficsplit": "ts",
|
||||
"job": "job",
|
||||
"authority": "au"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ let applicationHtml = (
|
|||
<Route
|
||||
path={`${pathPrefix}/namespaces/:namespace/statefulsets/:statefulset`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceDetail} />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/namespaces/:namespace/trafficsplits/:trafficsplit`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceDetail} />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/namespaces/:namespace/jobs/:job`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceDetail} />} />
|
||||
|
|
@ -90,6 +93,9 @@ let applicationHtml = (
|
|||
<Route
|
||||
path={`${pathPrefix}/namespaces`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="namespace" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/trafficsplits`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="trafficsplit" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/deployments`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="deployment" />} />
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ func NewServer(
|
|||
server.router.GET("/namespaces/:namespace", handler.handleIndex)
|
||||
server.router.GET("/daemonsets", handler.handleIndex)
|
||||
server.router.GET("/statefulsets", handler.handleIndex)
|
||||
server.router.GET("/trafficsplits", handler.handleIndex)
|
||||
server.router.GET("/jobs", handler.handleIndex)
|
||||
server.router.GET("/deployments", handler.handleIndex)
|
||||
server.router.GET("/replicationcontrollers", handler.handleIndex)
|
||||
|
|
@ -110,6 +111,7 @@ func NewServer(
|
|||
server.router.GET("/namespaces/:namespace/pods/:pod", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace/daemonsets/:daemonset", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace/statefulsets/:statefulset", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace/trafficsplits/:trafficsplit", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace/deployments/:deployment", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace/jobs/:job", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace/replicationcontrollers/:replicationcontroller", handler.handleIndex)
|
||||
|
|
|
|||
Loading…
Reference in New Issue