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));