mirror of https://github.com/linkerd/linkerd2.git
Display proxy container errors in the Web UI (#1130)
* Display proxy container errors in the Web UI Add an error modal to display pod errors Add icon to data tables to indicate errors are present Display errors on the Service Mesh Overview Page and all the resource pages
This commit is contained in:
parent
5c42e4e22b
commit
0ed40288e5
|
@ -85,3 +85,28 @@ td .status-dot {
|
|||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* error indicator and modal */
|
||||
.conduit-error-icon {
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
color: var(--siennared);
|
||||
}
|
||||
|
||||
.conduit-pod-error {
|
||||
margin-top: calc(2 * var(--base-width));
|
||||
margin-bottom: calc(3 * var(--base-width));
|
||||
|
||||
& p {
|
||||
line-height: 8px;
|
||||
}
|
||||
|
||||
& .error-text {
|
||||
padding: var(--base-width);
|
||||
border-radius: calc(0.5 * var(--base-width));
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
background-color: #696969;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Icon, Modal } from 'antd';
|
||||
|
||||
export default class ErrorModal extends React.Component {
|
||||
static propTypes = {
|
||||
errors: PropTypes.shape({}).isRequired,
|
||||
resourceName: PropTypes.string.isRequired,
|
||||
resourceType: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
showModal = () => {
|
||||
Modal.error({
|
||||
title: `Errors in ${this.props.resourceType} ${this.props.resourceName}`,
|
||||
width: 800,
|
||||
maskClosable: true,
|
||||
content: this.renderPodErrors(this.props.errors),
|
||||
onOk() {},
|
||||
});
|
||||
}
|
||||
|
||||
renderContainerErrors = errorsByContainer => {
|
||||
return _.map(errorsByContainer, (errors, container) => (
|
||||
<div key={`error-${container}`}>
|
||||
<p>Container: {container}</p>
|
||||
<p>Image: {_.get(errors, [0, "image"])}</p>
|
||||
<div className="error-text">
|
||||
{
|
||||
_.map(errors, (er, i) =>
|
||||
<code key={`error-msg-${i}`}>{er.message}</code>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
renderPodErrors = podErrors => {
|
||||
let errorsByPodAndContainer = _(podErrors)
|
||||
.keys()
|
||||
.sortBy()
|
||||
.map(pod => {
|
||||
return {
|
||||
pod: pod,
|
||||
byContainer: _(podErrors[pod].errors)
|
||||
.groupBy( "container.container")
|
||||
.mapValues(v => _.map(v, "container"))
|
||||
.value()
|
||||
};
|
||||
}).value();
|
||||
|
||||
return _.map(errorsByPodAndContainer, err => {
|
||||
return (
|
||||
<div className="conduit-pod-error" key={err.pod}>
|
||||
<h3>Pod: {err.pod}</h3>
|
||||
{this.renderContainerErrors(err.byContainer)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Icon type="warning" className="conduit-error-icon" onClick={this.showModal} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import BaseTable from './BaseTable.jsx';
|
||||
import ErrorModal from './ErrorModal.jsx';
|
||||
import GrafanaLink from './GrafanaLink.jsx';
|
||||
import { processedMetricsPropType } from './util/MetricUtils.js';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -60,12 +61,13 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
defaultSortOrder: 'ascend',
|
||||
sorter: (a, b) => (a.name || "").localeCompare(b.name),
|
||||
render: row => {
|
||||
let nameContents;
|
||||
if (resource.toLowerCase() === "namespace") {
|
||||
return <ConduitLink to={"/namespaces/" + row.name}>{row.name}</ConduitLink>;
|
||||
nameContents = <ConduitLink to={"/namespaces/" + row.name}>{row.name}</ConduitLink>;
|
||||
} else if (!row.added) {
|
||||
return row.name;
|
||||
nameContents = row.name;
|
||||
} else {
|
||||
return (
|
||||
nameContents = (
|
||||
<GrafanaLink
|
||||
name={row.name}
|
||||
namespace={row.namespace}
|
||||
|
@ -73,6 +75,12 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
ConduitLink={ConduitLink} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{nameContents}
|
||||
{ _.isEmpty(row.errors) ? null : <ErrorModal errors={row.errors} resourceName={row.name} resourceType={resource} /> }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||
import CallToAction from './CallToAction.jsx';
|
||||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import ErrorModal from './ErrorModal.jsx';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import Metric from './Metric.jsx';
|
||||
import { numericSort } from './util/Utils.js';
|
||||
|
@ -41,11 +42,19 @@ const getClassification = (meshedPodCount, failedPodCount) => {
|
|||
const namespacesColumns = ConduitLink => [
|
||||
{
|
||||
title: "Namespace",
|
||||
dataIndex: "namespace",
|
||||
key: "namespace",
|
||||
defaultSortOrder: "ascend",
|
||||
sorter: (a, b) => (a.namespace || "").localeCompare(b.namespace),
|
||||
render: d => <ConduitLink to={"/namespaces/" + d}>{d}</ConduitLink>
|
||||
render: d => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ConduitLink to={"/namespaces/" + d.namespace}>{d.namespace}</ConduitLink>
|
||||
{ _.isEmpty(d.errors) ? null :
|
||||
<ErrorModal errors={d.errors} resourceName={d.namespace} resourceType="namespace" />
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Meshed pods",
|
||||
|
@ -62,7 +71,8 @@ const namespacesColumns = ConduitLink => [
|
|||
let containerWidth = 132;
|
||||
let percent = row.meshedPercent.get();
|
||||
let barWidth = percent < 0 ? 0 : Math.round(percent * containerWidth);
|
||||
let barType = getClassification(row.meshedPods, row.failedPods);
|
||||
let barType = _.isEmpty(row.errors) ?
|
||||
getClassification(row.meshedPods, row.failedPods) : "poor";
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
@ -165,7 +175,8 @@ class ServiceMesh extends React.Component {
|
|||
meshedPercent: new Percentage(meshedPods, totalPods),
|
||||
meshedPods,
|
||||
totalPods,
|
||||
failedPods
|
||||
failedPods,
|
||||
errors: ns.errorsByPod
|
||||
};
|
||||
});
|
||||
return _.compact(dataPlaneNamepaces);
|
||||
|
|
|
@ -117,7 +117,8 @@ const processStatTable = table => {
|
|||
successRate: getSuccessRate(row),
|
||||
latency: getLatency(row),
|
||||
tlsRequestPercent: getTlsRequestPercentage(row),
|
||||
added: row.meshedPodCount === row.runningPodCount
|
||||
added: row.meshedPodCount === row.runningPodCount,
|
||||
errors: row.errorsByPod
|
||||
};
|
||||
})
|
||||
.compact()
|
||||
|
|
|
@ -26,7 +26,8 @@ describe('MetricUtils', () => {
|
|||
P95: 2,
|
||||
P99: 7
|
||||
},
|
||||
added: true
|
||||
added: true,
|
||||
errors: {}
|
||||
}
|
||||
];
|
||||
expect(result).to.have.length(1);
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
},
|
||||
"timeWindow": "1m",
|
||||
"runningPodCount": "1",
|
||||
"failedPodCount": null
|
||||
"failedPodCount": null,
|
||||
"errorsByPod": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue