github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/reports/containers/network/index.tsx (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 import { deviation as d3Deviation, mean as d3Mean } from "d3"; 12 import _, { capitalize } from "lodash"; 13 import moment from "moment"; 14 import React, { Fragment } from "react"; 15 import { Helmet } from "react-helmet"; 16 import { connect } from "react-redux"; 17 import { createSelector } from "reselect"; 18 import { withRouter, RouteComponentProps } from "react-router-dom"; 19 20 import { refreshLiveness, refreshNodes } from "src/redux/apiReducers"; 21 import { LivenessStatus, NodesSummary, nodesSummarySelector, selectLivenessRequestStatus, selectNodeRequestStatus } from "src/redux/nodes"; 22 import { AdminUIState } from "src/redux/state"; 23 import { LongToMoment, NanoToMilli } from "src/util/convert"; 24 import { FixLong } from "src/util/fixLong"; 25 import { trackFilter, trackCollapseNodes } from "src/util/analytics"; 26 import { getFilters, localityToString, NodeFilterList, NodeFilterListProps } from "src/views/reports/components/nodeFilterList"; 27 import Loading from "src/views/shared/components/loading"; 28 import { Latency } from "./latency"; 29 import { Legend } from "./legend"; 30 import Sort from "./sort"; 31 import { getMatchParamByName } from "src/util/query"; 32 import "./network.styl"; 33 34 interface NetworkOwnProps { 35 nodesSummary: NodesSummary; 36 nodeSummaryErrors: Error[]; 37 refreshNodes: typeof refreshNodes; 38 refreshLiveness: typeof refreshLiveness; 39 } 40 41 export interface Identity { 42 nodeID: number; 43 address: string; 44 locality?: string; 45 updatedAt: moment.Moment; 46 } 47 48 export interface NoConnection { 49 from: Identity; 50 to: Identity; 51 } 52 53 type NetworkProps = NetworkOwnProps & RouteComponentProps; 54 55 export interface NetworkFilter { 56 [key: string]: Array<string>; 57 } 58 59 export interface NetworkSort { 60 id: string; 61 filters: Array<{ name: string, address: string }>; 62 } 63 64 interface INetworkState { 65 collapsed: boolean; 66 filter: NetworkFilter|null; 67 } 68 69 function contentAvailable(nodesSummary: NodesSummary) { 70 return ( 71 !_.isUndefined(nodesSummary) && 72 !_.isEmpty(nodesSummary.nodeStatuses) && 73 !_.isEmpty(nodesSummary.nodeStatusByID) && 74 !_.isEmpty(nodesSummary.nodeIDs) 75 ); 76 } 77 78 export function getValueFromString(key: string, params: string, fullString?: boolean) { 79 if (!params) { 80 return; 81 } 82 const result = params.match(new RegExp(key + "=([^,#]*)")); 83 if (!result) { 84 return; 85 } 86 return fullString ? result[0] : result[1]; 87 } 88 89 /** 90 * Renders the Network Diagnostics Report page. 91 */ 92 export class Network extends React.Component<NetworkProps, INetworkState> { 93 state: INetworkState = { 94 collapsed: false, 95 filter: null, 96 }; 97 98 refresh(props = this.props) { 99 props.refreshLiveness(); 100 props.refreshNodes(); 101 } 102 103 componentDidMount() { 104 // Refresh nodes status query when mounting. 105 this.refresh(); 106 } 107 108 componentDidUpdate(prevProps: NetworkProps) { 109 if (!_.isEqual(this.props.location, prevProps.location)) { 110 this.refresh(this.props); 111 } 112 } 113 114 onChangeCollapse = (collapsed: boolean) => { 115 trackCollapseNodes(collapsed); 116 this.setState({ collapsed }); 117 } 118 119 onChangeFilter = (key: string, value: string) => { 120 const { filter } = this.state; 121 const newFilter = filter ? filter : {}; 122 const data = newFilter[key] || []; 123 const values = data.indexOf(value) === -1 ? [...data, value] : data.length === 1 ? null : data.filter((m: string|number) => m !== value); 124 trackFilter(capitalize(key), value); 125 this.setState({ 126 filter: { 127 ...newFilter, 128 [key]: values, 129 }, 130 }); 131 } 132 133 deselectFilterByKey = (key: string) => { 134 const { filter } = this.state; 135 const newFilter = filter ? filter : {}; 136 trackFilter(capitalize(key), "deselect all"); 137 this.setState({ 138 filter: { 139 ...newFilter, 140 [key]: null, 141 }, 142 }); 143 } 144 145 filteredDisplayIdentities = (displayIdentities: Identity[]) => { 146 const { filter } = this.state; 147 let data: Identity[] = []; 148 let selectedIndex = 0; 149 if (!filter || Object.keys(filter).length === 0 || Object.keys(filter).every(x => filter[x] === null)) { 150 return displayIdentities; 151 } 152 displayIdentities.forEach(identities => { 153 Object.keys(filter).forEach((key, index) => { 154 const value = getValueFromString(key, key === "cluster" ? `cluster=${identities.nodeID.toString()}` : identities.locality); 155 if ((!data.length || selectedIndex === index) && filter[key] && filter[key].indexOf(value) !== -1) { 156 data.push(identities); 157 selectedIndex = index; 158 } else if (filter[key]) { 159 data = data.filter(identity => filter[key].indexOf(getValueFromString(key, key === "cluster" ? `cluster=${identity.nodeID.toString()}` : identity.locality)) !== -1); 160 } 161 }); 162 }); 163 return data; 164 } 165 166 renderLatencyTable( 167 latencies: number[], 168 staleIDs: Set<number>, 169 nodesSummary: NodesSummary, 170 displayIdentities: Identity[], 171 noConnections: NoConnection[], 172 ) { 173 const { match } = this.props; 174 const nodeId = getMatchParamByName(match, "node_id"); 175 const { collapsed, filter } = this.state; 176 const mean = d3Mean(latencies); 177 const sortParams = this.getSortParams(displayIdentities); 178 let stddev = d3Deviation(latencies); 179 if (_.isUndefined(stddev)) { 180 stddev = 0; 181 } 182 // If there is no stddev, we should not display a legend. So there is no 183 // need to set these values. 184 const stddevPlus1 = stddev > 0 ? mean + stddev : 0; 185 const stddevPlus2 = stddev > 0 ? stddevPlus1 + stddev : 0; 186 const stddevMinus1 = stddev > 0 ? _.max([mean - stddev, 0]) : 0; 187 const stddevMinus2 = stddev > 0 ? _.max([stddevMinus1 - stddev, 0]) : 0; 188 const latencyTable = ( 189 <Latency 190 displayIdentities={this.filteredDisplayIdentities(displayIdentities)} 191 staleIDs={staleIDs} 192 multipleHeader={nodeId !== "cluster"} 193 node_id={nodeId} 194 collapsed={collapsed} 195 nodesSummary={nodesSummary} 196 std={{ 197 stddev, 198 stddevMinus2, 199 stddevMinus1, 200 stddevPlus1, 201 stddevPlus2, 202 }} 203 /> 204 ); 205 206 if (stddev === 0) { 207 return latencyTable; 208 } 209 210 // legend is just a quick table showing the standard deviation values. 211 return [ 212 <Sort 213 onChangeCollapse={this.onChangeCollapse} 214 collapsed={collapsed} 215 sort={sortParams} 216 filter={filter} 217 onChangeFilter={this.onChangeFilter} 218 deselectFilterByKey={this.deselectFilterByKey} 219 />, 220 <div className="section"> 221 <Legend 222 stddevMinus2={stddevMinus2} 223 stddevMinus1={stddevMinus1} 224 mean={mean} 225 stddevPlus1={stddevPlus1} 226 stddevPlus2={stddevPlus2} 227 noConnections={noConnections} 228 /> 229 {latencyTable} 230 </div>, 231 ]; 232 } 233 234 getSortParams = (data: Identity[]) => { 235 const sort: NetworkSort[] = []; 236 const searchQuery = (params: string) => `cluster,${params}`; 237 data.forEach(values => { 238 const localities = searchQuery(values.locality).split(","); 239 localities.forEach((locality: string) => { 240 if (locality !== "") { 241 const value = locality.match(/^\w+/gi) ? locality.match(/^\w+/gi)[0] : null; 242 if (!sort.some(x => x.id === value)) { 243 const sortValue: NetworkSort = {id: value, filters: []}; 244 data.forEach(item => { 245 const valueLocality = searchQuery(values.locality).split(","); 246 const itemLocality = searchQuery(item.locality); 247 valueLocality.forEach(val => { 248 const itemLocalitySplited = val.match(/^\w+/gi) ? val.match(/^\w+/gi)[0] : null; 249 if (val === "cluster" && value === "cluster") { 250 sortValue.filters = [...sortValue.filters, { 251 name: item.nodeID.toString(), 252 address: item.address, 253 }]; 254 } else if (itemLocalitySplited === value && !sortValue.filters.reduce((accumulator, vendor) => (accumulator || vendor.name === getValueFromString(value, itemLocality)), false)) { 255 sortValue.filters = [...sortValue.filters, { 256 name: getValueFromString(value, itemLocality), 257 address: item.address, 258 }]; 259 } 260 }); 261 }); 262 sort.push(sortValue); 263 } 264 } 265 }); 266 }); 267 return sort; 268 } 269 270 getDisplayIdentities = (healthyIDsContext: _.CollectionChain<number>, staleIDsContext: _.CollectionChain<number>, identityByID: Map<number, Identity>) => { 271 const { match } = this.props; 272 const nodeId = getMatchParamByName(match, "node_id"); 273 const identityContent = healthyIDsContext.union(staleIDsContext.value()).map(nodeID => identityByID.get(nodeID)).sortBy(identity => identity.nodeID); 274 const sort = this.getSortParams(identityContent.value()); 275 if (sort.some(x => (x.id === nodeId))) { 276 return identityContent.sortBy(identity => getValueFromString(nodeId, identity.locality, true)).value(); 277 } 278 return identityContent.value(); 279 } 280 281 renderContent(nodesSummary: NodesSummary, filters: NodeFilterListProps) { 282 if (!contentAvailable(nodesSummary)) { 283 return null; 284 } 285 // List of node identities. 286 const identityByID: Map<number, Identity> = new Map(); 287 _.forEach(nodesSummary.nodeStatuses, status => { 288 identityByID.set(status.desc.node_id, { 289 nodeID: status.desc.node_id, 290 address: status.desc.address.address_field, 291 locality: localityToString(status.desc.locality), 292 updatedAt: LongToMoment(status.updated_at), 293 }); 294 }); 295 296 // Calculate the mean and sampled standard deviation. 297 let healthyIDsContext = _.chain(nodesSummary.nodeIDs) 298 .filter( 299 nodeID => 300 nodesSummary.livenessStatusByNodeID[nodeID] === LivenessStatus.LIVE, 301 ) 302 .filter(nodeID => !_.isNil(nodesSummary.nodeStatusByID[nodeID].activity)) 303 .map(nodeID => Number.parseInt(nodeID, 0)); 304 let staleIDsContext = _.chain(nodesSummary.nodeIDs) 305 .filter( 306 nodeID => 307 nodesSummary.livenessStatusByNodeID[nodeID] === 308 LivenessStatus.UNAVAILABLE, 309 ) 310 .map(nodeID => Number.parseInt(nodeID, 0)); 311 if (!_.isNil(filters.nodeIDs) && filters.nodeIDs.size > 0) { 312 healthyIDsContext = healthyIDsContext.filter(nodeID => 313 filters.nodeIDs.has(nodeID), 314 ); 315 staleIDsContext = staleIDsContext.filter(nodeID => 316 filters.nodeIDs.has(nodeID), 317 ); 318 } 319 if (!_.isNil(filters.localityRegex)) { 320 healthyIDsContext = healthyIDsContext.filter(nodeID => 321 filters.localityRegex.test( 322 localityToString(nodesSummary.nodeStatusByID[nodeID].desc.locality), 323 ), 324 ); 325 staleIDsContext = staleIDsContext.filter(nodeID => 326 filters.localityRegex.test( 327 localityToString(nodesSummary.nodeStatusByID[nodeID].desc.locality), 328 ), 329 ); 330 } 331 const healthyIDs = healthyIDsContext.value(); 332 const staleIDs = new Set(staleIDsContext.value()); 333 const displayIdentities: Identity[] = this.getDisplayIdentities(healthyIDsContext, staleIDsContext, identityByID); 334 const latencies = _.flatMap(healthyIDs, nodeIDa => ( 335 _.chain(healthyIDs) 336 .without(nodeIDa) 337 .map(nodeIDb => nodesSummary.nodeStatusByID[nodeIDa].activity[nodeIDb]) 338 .filter(activity => !_.isNil(activity) && !_.isNil(activity.latency)) 339 .map(activity => NanoToMilli(FixLong(activity.latency).toNumber())) 340 .filter(ms => _.isFinite(ms) && ms > 0) 341 .value() 342 )); 343 344 const noConnections: NoConnection[] = _.flatMap(healthyIDs, nodeIDa => 345 _.chain(nodesSummary.nodeStatusByID[nodeIDa].activity) 346 .keys() 347 .map(nodeIDb => Number.parseInt(nodeIDb, 10)) 348 .difference(healthyIDs) 349 .map(nodeIDb => ({ 350 from: identityByID.get(nodeIDa), 351 to: identityByID.get(nodeIDb), 352 })) 353 .sortBy(noConnection => noConnection.to.nodeID) 354 .sortBy(noConnection => noConnection.to.locality) 355 .sortBy(noConnection => noConnection.from.nodeID) 356 .sortBy(noConnection => noConnection.from.locality) 357 .value(), 358 ); 359 360 let content: JSX.Element | JSX.Element[]; 361 if (_.isEmpty(healthyIDs)) { 362 content = <h2 className="base-heading">No healthy nodes match the filters</h2>; 363 } else if (latencies.length < 1) { 364 content = <h2 className="base-heading">Cannot show latency chart without two healthy nodes.</h2>; 365 } else { 366 content = this.renderLatencyTable( 367 latencies, 368 staleIDs, 369 nodesSummary, 370 displayIdentities, 371 noConnections, 372 ); 373 } 374 return [ 375 content, 376 // staleTable(staleIdentities), 377 // noConnectionTable(noConnections), 378 ]; 379 } 380 381 render() { 382 const { nodesSummary, location } = this.props; 383 const filters = getFilters(location); 384 return ( 385 <Fragment> 386 <Helmet title="Network Diagnostics | Debug" /> 387 <div className="section"> 388 <h1 className="base-heading">Network Diagnostics</h1> 389 </div> 390 <Loading 391 loading={!contentAvailable(nodesSummary)} 392 error={this.props.nodeSummaryErrors} 393 className="loading-image loading-image__spinner-left loading-image__spinner-left__padded" 394 render={() => ( 395 <div> 396 <NodeFilterList 397 nodeIDs={filters.nodeIDs} 398 localityRegex={filters.localityRegex} 399 /> 400 {this.renderContent(nodesSummary, filters)} 401 </div> 402 )} 403 /> 404 </Fragment> 405 ); 406 } 407 } 408 409 const nodeSummaryErrors = createSelector( 410 selectNodeRequestStatus, 411 selectLivenessRequestStatus, 412 (nodes, liveness) => [nodes.lastError, liveness.lastError], 413 ); 414 415 const mapStateToProps = (state: AdminUIState) => ({ 416 nodesSummary: nodesSummarySelector(state), 417 nodeSummaryErrors: nodeSummaryErrors(state), 418 }); 419 420 const mapDispatchToProps = { 421 refreshNodes, 422 refreshLiveness, 423 }; 424 425 export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Network));