mirror of https://github.com/linkerd/linkerd2.git
Add top request table to resource detail page (#1507)
Includes a substantial refactor of Top.jsx to move the websocket and top-request-aggregation code into a self-contained module so that this code can be shared by /top and by each resource detail page. (This refactor also helps separate concerns in that page; since that page also makes 10 second requests to the stat api to populate the autocompletes in the form). The TopModule uses the startTap prop to figure out whether it should start a websocket connection and make a tap request when mounted. (This is because the resource detail pages start tapping immediately upon load, whereas /top can only start once you've entered a query. I've removed the spinner and the awaitingWebSocketConnection state field because that now belongs in the top module. I think a similar refactor of tap would be good before we re-add it.
This commit is contained in:
parent
211fca1806
commit
3fde755a8f
|
@ -7,6 +7,7 @@ import { processSingleResourceRollup } from './util/MetricUtils.js';
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import TopModule from './TopModule.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { resourceTypeToCamelCase, singularResource } from './util/Utils.js';
|
||||
import 'whatwg-fetch';
|
||||
|
@ -170,6 +171,12 @@ export class ResourceDetailBase extends React.Component {
|
|||
return <Spin size="large" />;
|
||||
}
|
||||
|
||||
let topQuery = {
|
||||
resource: this.state.resourceType + "/" + this.state.resourceName,
|
||||
namespace: this.state.namespace,
|
||||
maxRps: "1.0"
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-section">
|
||||
|
@ -179,6 +186,14 @@ export class ResourceDetailBase extends React.Component {
|
|||
api={this.api} />
|
||||
</div>
|
||||
|
||||
<div className="page-section">
|
||||
<TopModule
|
||||
pathPrefix={this.props.pathPrefix}
|
||||
query={topQuery}
|
||||
startTap={true}
|
||||
maxRowsToDisplay={10} />
|
||||
</div>
|
||||
|
||||
{ _.isEmpty(this.state.neighborMetrics.upstream) ? null : (
|
||||
<div className="page-section">
|
||||
<h2 className="subsection-header">Upstreams</h2>
|
||||
|
|
|
@ -42,7 +42,6 @@ class Tap extends React.Component {
|
|||
maxRps: defaultMaxRps
|
||||
},
|
||||
maxLinesToDisplay: 40,
|
||||
awaitingWebSocketConnection: false,
|
||||
tapRequestInProgress: false,
|
||||
pollingInterval: 10000,
|
||||
pendingRequests: false
|
||||
|
@ -70,7 +69,6 @@ class Tap extends React.Component {
|
|||
...query
|
||||
}));
|
||||
this.setState({
|
||||
awaitingWebSocketConnection: false,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
@ -250,9 +248,8 @@ class Tap extends React.Component {
|
|||
this.api.cancelCurrentRequests();
|
||||
}
|
||||
|
||||
startTapSteaming() {
|
||||
startTapStreaming() {
|
||||
this.setState({
|
||||
awaitingWebSocketConnection: true,
|
||||
tapRequestInProgress: true,
|
||||
tapResultsById: {},
|
||||
tapResultFilterOptions: this.getInitialTapFilterOptions()
|
||||
|
@ -270,14 +267,13 @@ class Tap extends React.Component {
|
|||
|
||||
stopTapStreaming() {
|
||||
this.setState({
|
||||
tapRequestInProgress: false,
|
||||
awaitingWebSocketConnection: false
|
||||
tapRequestInProgress: false
|
||||
});
|
||||
}
|
||||
|
||||
handleTapStart = e => {
|
||||
e.preventDefault();
|
||||
this.startTapSteaming();
|
||||
this.startTapStreaming();
|
||||
}
|
||||
|
||||
handleTapStop = () => {
|
||||
|
@ -336,7 +332,6 @@ class Tap extends React.Component {
|
|||
<PageHeader header="Tap" />
|
||||
<TapQueryForm
|
||||
tapRequestInProgress={this.state.tapRequestInProgress}
|
||||
awaitingWebSocketConnection={this.state.awaitingWebSocketConnection}
|
||||
handleTapStart={this.handleTapStart}
|
||||
handleTapStop={this.handleTapStop}
|
||||
resourcesByNs={this.state.resourcesByNs}
|
||||
|
|
|
@ -21,7 +21,6 @@ const getResourceList = (resourcesByNs, ns) => {
|
|||
};
|
||||
export default class TapQueryForm extends React.Component {
|
||||
static propTypes = {
|
||||
awaitingWebSocketConnection: PropTypes.bool.isRequired,
|
||||
enableAdvancedForm: PropTypes.bool,
|
||||
handleTapStart: PropTypes.func.isRequired,
|
||||
handleTapStop: PropTypes.func.isRequired,
|
||||
|
@ -249,14 +248,10 @@ export default class TapQueryForm extends React.Component {
|
|||
<Col span={colSpan}>
|
||||
<Form.Item>
|
||||
{
|
||||
this.props.tapRequestInProgress ?
|
||||
<Button type="primary" className="tap-stop" onClick={this.props.handleTapStop}>Stop</Button> :
|
||||
<Button type="primary" className="tap-start" onClick={this.props.handleTapStart}>Start</Button>
|
||||
}
|
||||
{
|
||||
this.props.awaitingWebSocketConnection ?
|
||||
<Icon type="loading" style={{ paddingLeft: rowGutter, fontSize: 20, color: '#08c' }} /> : null
|
||||
}
|
||||
this.props.tapRequestInProgress ?
|
||||
<Button type="primary" className="tap-stop" onClick={this.props.handleTapStop}>Stop</Button> :
|
||||
<Button type="primary" className="tap-start" onClick={this.props.handleTapStart}>Start</Button>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -267,8 +262,9 @@ export default class TapQueryForm extends React.Component {
|
|||
<Button
|
||||
className="tap-form-toggle"
|
||||
onClick={() => this.toggleAdvancedForm(!this.state.showAdvancedForm)}>
|
||||
{ this.state.showAdvancedForm ?
|
||||
"Hide filters" : "Show more request filters" } <Icon type={this.state.showAdvancedForm ? 'up' : 'down'} />
|
||||
{
|
||||
this.state.showAdvancedForm ? "Hide filters" : "Show more request filters"
|
||||
} <Icon type={this.state.showAdvancedForm ? 'up' : 'down'} />
|
||||
</Button>
|
||||
|
||||
{ !this.state.showAdvancedForm ? null : this.renderAdvancedTapForm() }
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import _ from 'lodash';
|
||||
import { defaultMaxRps } from './util/TapUtils.jsx';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import Percentage from './util/Percentage.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TapQueryCliCmd from './TapQueryCliCmd.jsx';
|
||||
import TapQueryForm from './TapQueryForm.jsx';
|
||||
import TopEventTable from './TopEventTable.jsx';
|
||||
import TopModule from './TopModule.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { defaultMaxRps, processTapEvent } from './util/TapUtils.jsx';
|
||||
import './../../css/tap.css';
|
||||
|
||||
class Top extends React.Component {
|
||||
|
@ -25,8 +24,6 @@ class Top extends React.Component {
|
|||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
|
||||
this.state = {
|
||||
tapResultsById: {},
|
||||
topEventIndex: {},
|
||||
error: null,
|
||||
resourcesByNs: {},
|
||||
authoritiesByNs: {},
|
||||
|
@ -41,10 +38,8 @@ class Top extends React.Component {
|
|||
authority: "",
|
||||
maxRps: defaultMaxRps
|
||||
},
|
||||
maxRowsToStore: 40,
|
||||
awaitingWebSocketConnection: false,
|
||||
tapRequestInProgress: false,
|
||||
pollingInterval: 10000,
|
||||
tapRequestInProgress: false,
|
||||
pendingRequests: false
|
||||
};
|
||||
}
|
||||
|
@ -54,51 +49,9 @@ class Top extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.ws) {
|
||||
this.ws.close(1000);
|
||||
}
|
||||
this.stopTapStreaming();
|
||||
this.stopServerPolling();
|
||||
}
|
||||
|
||||
onWebsocketOpen = () => {
|
||||
let query = _.cloneDeep(this.state.query);
|
||||
query.maxRps = parseFloat(query.maxRps);
|
||||
|
||||
this.ws.send(JSON.stringify({
|
||||
id: "top-web",
|
||||
...query
|
||||
}));
|
||||
this.setState({
|
||||
awaitingWebSocketConnection: false,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
||||
onWebsocketRecv = e => {
|
||||
this.indexTapResult(e.data);
|
||||
}
|
||||
|
||||
onWebsocketClose = e => {
|
||||
this.stopTapStreaming();
|
||||
|
||||
if (!e.wasClean) {
|
||||
this.setState({
|
||||
error: {
|
||||
error: `Websocket [${e.code}] ${e.reason}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onWebsocketError = e => {
|
||||
this.setState({
|
||||
error: { error: e.message }
|
||||
});
|
||||
|
||||
this.stopTapStreaming();
|
||||
}
|
||||
|
||||
getResourcesByNs(rsp) {
|
||||
let statTables = _.get(rsp, [0, "ok", "statTables"]);
|
||||
let authoritiesByNs = {};
|
||||
|
@ -127,142 +80,6 @@ class Top extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
parseTapResult = data => {
|
||||
let d = processTapEvent(data);
|
||||
|
||||
if (d.eventType === "responseEnd") {
|
||||
d.latency = parseFloat(d.http.responseEnd.sinceRequestInit.replace("s", ""));
|
||||
d.completed = true;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
topEventKey = event => {
|
||||
return [event.source.str, event.destination.str, event.http.requestInit.path].join("_");
|
||||
}
|
||||
|
||||
initialTopResult(d, eventKey) {
|
||||
return {
|
||||
count: 1,
|
||||
best: d.responseEnd.latency,
|
||||
worst: d.responseEnd.latency,
|
||||
last: d.responseEnd.latency,
|
||||
success: !d.success ? 0 : 1,
|
||||
failure: !d.success ? 1 : 0,
|
||||
successRate: !d.success ? new Percentage(0, 1) : new Percentage(1, 1),
|
||||
source: d.requestInit.source,
|
||||
sourceLabels: d.requestInit.sourceMeta.labels,
|
||||
destination: d.requestInit.destination,
|
||||
destinationLabels: d.requestInit.destinationMeta.labels,
|
||||
path: d.requestInit.http.requestInit.path,
|
||||
key: eventKey,
|
||||
lastUpdated: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
incrementTopResult(d, result) {
|
||||
result.count++;
|
||||
if (!d.success) {
|
||||
result.failure++;
|
||||
} else {
|
||||
result.success++;
|
||||
}
|
||||
result.successRate = new Percentage(result.success, result.success + result.failure);
|
||||
|
||||
result.last = d.responseEnd.latency;
|
||||
if (d.responseEnd.latency < result.best) {
|
||||
result.best = d.responseEnd.latency;
|
||||
}
|
||||
if (d.responseEnd.latency > result.worst) {
|
||||
result.worst = d.responseEnd.latency;
|
||||
}
|
||||
|
||||
result.lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
indexTopResult = (d, topResults) => {
|
||||
let eventKey = this.topEventKey(d.requestInit);
|
||||
this.addSuccessCount(d);
|
||||
|
||||
if (!topResults[eventKey]) {
|
||||
topResults[eventKey] = this.initialTopResult(d, eventKey);
|
||||
} else {
|
||||
this.incrementTopResult(d, topResults[eventKey]);
|
||||
}
|
||||
|
||||
if (_.size(topResults) > this.state.maxRowsToStore) {
|
||||
this.deleteOldestIndexedResult(topResults);
|
||||
}
|
||||
|
||||
return topResults;
|
||||
}
|
||||
|
||||
indexTapResult = data => {
|
||||
// keep an index of tap results by id until the request is complete.
|
||||
// when the request has completed, add it to the aggregated Top counts and
|
||||
// discard the individual tap result
|
||||
let resultIndex = this.state.tapResultsById;
|
||||
let d = this.parseTapResult(data);
|
||||
|
||||
if (_.isNil(resultIndex[d.id])) {
|
||||
// don't let tapResultsById grow unbounded
|
||||
if (_.size(resultIndex) > this.state.maxRowsToStore) {
|
||||
this.deleteOldestIndexedResult(resultIndex);
|
||||
}
|
||||
|
||||
resultIndex[d.id] = {};
|
||||
}
|
||||
resultIndex[d.id][d.eventType] = d;
|
||||
|
||||
// assumption: requests of a given id all share the same high level metadata
|
||||
resultIndex[d.id].base = d;
|
||||
resultIndex[d.id].lastUpdated = Date.now();
|
||||
|
||||
let topIndex = this.state.topEventIndex;
|
||||
if (d.completed) {
|
||||
// only add results into top if the request has completed
|
||||
// we can also now delete this result from the Tap result index
|
||||
topIndex = this.indexTopResult(resultIndex[d.id], topIndex);
|
||||
delete resultIndex[d.id];
|
||||
}
|
||||
|
||||
this.setState({
|
||||
tapResultsById: resultIndex,
|
||||
topEventIndex: topIndex
|
||||
});
|
||||
}
|
||||
|
||||
addSuccessCount = d => {
|
||||
// cope with the fact that gRPC failures are returned with HTTP status 200
|
||||
// and correctly classify gRPC failures as failures
|
||||
let success = parseInt(d.responseInit.http.responseInit.httpStatus, 10) < 500;
|
||||
if (success) {
|
||||
let grpcStatusCode = _.get(d, "responseEnd.http.responseEnd.eos.grpcStatusCode");
|
||||
if (!_.isNil(grpcStatusCode)) {
|
||||
success = grpcStatusCode === 0;
|
||||
} else if (!_.isNil(_.get(d, "responseEnd.http.responseEnd.eos.resetErrorCode"))) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
d.success = success;
|
||||
}
|
||||
|
||||
deleteOldestIndexedResult = resultIndex => {
|
||||
let oldest = Date.now();
|
||||
let oldestId = "";
|
||||
|
||||
_.each(resultIndex, (res, id) => {
|
||||
if (res.lastUpdated < oldest) {
|
||||
oldest = res.lastUpdated;
|
||||
oldestId = id;
|
||||
}
|
||||
});
|
||||
|
||||
delete resultIndex[oldestId];
|
||||
}
|
||||
|
||||
startServerPolling() {
|
||||
this.loadFromServer();
|
||||
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
|
||||
|
@ -273,40 +90,6 @@ class Top extends React.Component {
|
|||
this.api.cancelCurrentRequests();
|
||||
}
|
||||
|
||||
startTapSteaming() {
|
||||
this.setState({
|
||||
awaitingWebSocketConnection: true,
|
||||
tapRequestInProgress: true,
|
||||
tapResultsById: {},
|
||||
topEventIndex: {}
|
||||
});
|
||||
|
||||
let protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
let tapWebSocket = `${protocol}://${window.location.host}${this.props.pathPrefix}/api/tap`;
|
||||
|
||||
this.ws = new WebSocket(tapWebSocket);
|
||||
this.ws.onmessage = this.onWebsocketRecv;
|
||||
this.ws.onclose = this.onWebsocketClose;
|
||||
this.ws.onopen = this.onWebsocketOpen;
|
||||
this.ws.onerror = this.onWebsocketError;
|
||||
}
|
||||
|
||||
stopTapStreaming() {
|
||||
this.setState({
|
||||
tapRequestInProgress: false,
|
||||
awaitingWebSocketConnection: false
|
||||
});
|
||||
}
|
||||
|
||||
handleTapStart = e => {
|
||||
e.preventDefault();
|
||||
this.startTapSteaming();
|
||||
}
|
||||
|
||||
handleTapStop = () => {
|
||||
this.ws.close(1000);
|
||||
}
|
||||
|
||||
loadFromServer() {
|
||||
if (this.state.pendingRequests) {
|
||||
return; // don't make more requests if the ones we sent haven't completed
|
||||
|
@ -347,29 +130,39 @@ class Top extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let tableRows = _.values(this.state.topEventIndex);
|
||||
handleTapStart = () => {
|
||||
this.setState({
|
||||
tapRequestInProgress: true
|
||||
});
|
||||
}
|
||||
|
||||
handleTapStop = () => {
|
||||
this.setState({
|
||||
tapRequestInProgress: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{!this.state.error ? null :
|
||||
<ErrorBanner message={this.state.error} onHideMessage={() => this.setState({ error: null })} />}
|
||||
|
||||
<PageHeader header="Top" />
|
||||
<TapQueryForm
|
||||
enableAdvancedForm={false}
|
||||
tapRequestInProgress={this.state.tapRequestInProgress}
|
||||
awaitingWebSocketConnection={this.state.awaitingWebSocketConnection}
|
||||
handleTapStart={this.handleTapStart}
|
||||
handleTapStop={this.handleTapStop}
|
||||
resourcesByNs={this.state.resourcesByNs}
|
||||
authoritiesByNs={this.state.authoritiesByNs}
|
||||
tapRequestInProgress={this.state.tapRequestInProgress}
|
||||
updateQuery={this.updateQuery}
|
||||
query={this.state.query} />
|
||||
|
||||
<TapQueryCliCmd cmdName="top" query={this.state.query} />
|
||||
|
||||
<TopEventTable tableRows={tableRows} />
|
||||
<TopModule
|
||||
pathPrefix={this.props.pathPrefix}
|
||||
query={this.state.query}
|
||||
startTap={this.state.tapRequestInProgress} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
import _ from 'lodash';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import Percentage from './util/Percentage.js';
|
||||
import { processTapEvent } from './util/TapUtils.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TopEventTable from './TopEventTable.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
|
||||
class TopModule extends React.Component {
|
||||
static propTypes = {
|
||||
maxRowsToDisplay: PropTypes.number,
|
||||
pathPrefix: PropTypes.string.isRequired,
|
||||
query: PropTypes.shape({}).isRequired,
|
||||
startTap: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
maxRowsToDisplay: 40 // max aggregated top rows to index and display in table
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
tapResultsById: {},
|
||||
topEventIndex: {},
|
||||
maxRowsToStore: 40 // max un-ended tap results to keep in index (pre-aggregation into the top counts)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.startTap) {
|
||||
this.startTapStreaming();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.startTap && !prevProps.startTap) {
|
||||
this.startTapStreaming();
|
||||
}
|
||||
if (!this.props.startTap && prevProps.startTap) {
|
||||
this.stopTapStreaming();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.stopTapStreaming();
|
||||
}
|
||||
|
||||
onWebsocketOpen = () => {
|
||||
let query = _.cloneDeep(this.props.query);
|
||||
query.maxRps = parseFloat(query.maxRps);
|
||||
|
||||
this.ws.send(JSON.stringify({
|
||||
id: "top-web",
|
||||
...query
|
||||
}));
|
||||
this.setState({
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
||||
onWebsocketRecv = e => {
|
||||
this.indexTapResult(e.data);
|
||||
}
|
||||
|
||||
onWebsocketClose = e => {
|
||||
if (!e.wasClean) {
|
||||
this.setState({
|
||||
error: { error: `Websocket [${e.code}] ${e.reason}` }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onWebsocketError = e => {
|
||||
this.setState({
|
||||
error: { error: e.message }
|
||||
});
|
||||
}
|
||||
|
||||
closeWebSocket = () => {
|
||||
if (this.ws) {
|
||||
this.ws.close(1000);
|
||||
}
|
||||
}
|
||||
|
||||
parseTapResult = data => {
|
||||
let d = processTapEvent(data);
|
||||
|
||||
if (d.eventType === "responseEnd") {
|
||||
d.latency = parseFloat(d.http.responseEnd.sinceRequestInit.replace("s", ""));
|
||||
d.completed = true;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
topEventKey = event => {
|
||||
return [event.source.str, event.destination.str, event.http.requestInit.path].join("_");
|
||||
}
|
||||
|
||||
initialTopResult(d, eventKey) {
|
||||
return {
|
||||
count: 1,
|
||||
best: d.responseEnd.latency,
|
||||
worst: d.responseEnd.latency,
|
||||
last: d.responseEnd.latency,
|
||||
success: !d.success ? 0 : 1,
|
||||
failure: !d.success ? 1 : 0,
|
||||
successRate: !d.success ? new Percentage(0, 1) : new Percentage(1, 1),
|
||||
source: d.requestInit.source,
|
||||
sourceLabels: d.requestInit.sourceMeta.labels,
|
||||
destination: d.requestInit.destination,
|
||||
destinationLabels: d.requestInit.destinationMeta.labels,
|
||||
path: d.requestInit.http.requestInit.path,
|
||||
key: eventKey,
|
||||
lastUpdated: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
incrementTopResult(d, result) {
|
||||
result.count++;
|
||||
if (!d.success) {
|
||||
result.failure++;
|
||||
} else {
|
||||
result.success++;
|
||||
}
|
||||
result.successRate = new Percentage(result.success, result.success + result.failure);
|
||||
|
||||
result.last = d.responseEnd.latency;
|
||||
if (d.responseEnd.latency < result.best) {
|
||||
result.best = d.responseEnd.latency;
|
||||
}
|
||||
if (d.responseEnd.latency > result.worst) {
|
||||
result.worst = d.responseEnd.latency;
|
||||
}
|
||||
|
||||
result.lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
indexTopResult = (d, topResults) => {
|
||||
let eventKey = this.topEventKey(d.requestInit);
|
||||
this.addSuccessCount(d);
|
||||
|
||||
if (!topResults[eventKey]) {
|
||||
topResults[eventKey] = this.initialTopResult(d, eventKey);
|
||||
} else {
|
||||
this.incrementTopResult(d, topResults[eventKey]);
|
||||
}
|
||||
|
||||
if (_.size(topResults) > this.props.maxRowsToDisplay) {
|
||||
this.deleteOldestIndexedResult(topResults);
|
||||
}
|
||||
|
||||
return topResults;
|
||||
}
|
||||
|
||||
indexTapResult = data => {
|
||||
// keep an index of tap results by id until the request is complete.
|
||||
// when the request has completed, add it to the aggregated Top counts and
|
||||
// discard the individual tap result
|
||||
let resultIndex = this.state.tapResultsById;
|
||||
let d = this.parseTapResult(data);
|
||||
|
||||
if (_.isNil(resultIndex[d.id])) {
|
||||
// don't let tapResultsById grow unbounded
|
||||
if (_.size(resultIndex) > this.state.maxRowsToStore) {
|
||||
this.deleteOldestIndexedResult(resultIndex);
|
||||
}
|
||||
|
||||
resultIndex[d.id] = {};
|
||||
}
|
||||
resultIndex[d.id][d.eventType] = d;
|
||||
|
||||
// assumption: requests of a given id all share the same high level metadata
|
||||
resultIndex[d.id].base = d;
|
||||
resultIndex[d.id].lastUpdated = Date.now();
|
||||
|
||||
let topIndex = this.state.topEventIndex;
|
||||
if (d.completed) {
|
||||
// only add results into top if the request has completed
|
||||
// we can also now delete this result from the Tap result index
|
||||
topIndex = this.indexTopResult(resultIndex[d.id], topIndex);
|
||||
delete resultIndex[d.id];
|
||||
}
|
||||
|
||||
this.setState({
|
||||
tapResultsById: resultIndex,
|
||||
topEventIndex: topIndex
|
||||
});
|
||||
}
|
||||
|
||||
addSuccessCount = d => {
|
||||
// cope with the fact that gRPC failures are returned with HTTP status 200
|
||||
// and correctly classify gRPC failures as failures
|
||||
let success = parseInt(d.responseInit.http.responseInit.httpStatus, 10) < 500;
|
||||
if (success) {
|
||||
let grpcStatusCode = _.get(d, "responseEnd.http.responseEnd.eos.grpcStatusCode");
|
||||
if (!_.isNil(grpcStatusCode)) {
|
||||
success = grpcStatusCode === 0;
|
||||
} else if (!_.isNil(_.get(d, "responseEnd.http.responseEnd.eos.resetErrorCode"))) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
d.success = success;
|
||||
}
|
||||
|
||||
deleteOldestIndexedResult = resultIndex => {
|
||||
let oldest = Date.now();
|
||||
let oldestId = "";
|
||||
|
||||
_.each(resultIndex, (res, id) => {
|
||||
if (res.lastUpdated < oldest) {
|
||||
oldest = res.lastUpdated;
|
||||
oldestId = id;
|
||||
}
|
||||
});
|
||||
|
||||
delete resultIndex[oldestId];
|
||||
}
|
||||
|
||||
startTapStreaming() {
|
||||
let protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
let tapWebSocket = `${protocol}://${window.location.host}${this.props.pathPrefix}/api/tap`;
|
||||
|
||||
this.ws = new WebSocket(tapWebSocket);
|
||||
this.ws.onmessage = this.onWebsocketRecv;
|
||||
this.ws.onclose = this.onWebsocketClose;
|
||||
this.ws.onopen = this.onWebsocketOpen;
|
||||
this.ws.onerror = this.onWebsocketError;
|
||||
}
|
||||
|
||||
stopTapStreaming() {
|
||||
this.closeWebSocket();
|
||||
}
|
||||
|
||||
banner = () => {
|
||||
if (!this.state.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <ErrorBanner message={this.state.error} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
let tableRows = _.values(this.state.topEventIndex);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.banner()}
|
||||
<TopEventTable tableRows={tableRows} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withContext(TopModule);
|
Loading…
Reference in New Issue