Explorar o código

[attu metrics view] topology

Signed-off-by: min.tian <min.tian.cn@gmail.com>
min.tian %!s(int64=2) %!d(string=hai) anos
pai
achega
58167f0ffd

+ 34 - 20
client/src/pages/systemHealthy/HealthyIndexOverview.tsx

@@ -1,7 +1,12 @@
 import { makeStyles, Theme } from '@material-ui/core';
-import { CHART_WIDTH, MAIN_VIEW_WIDTH } from './consts';
+import {
+  CHART_WIDTH,
+  LINE_CHART_LARGE_HEIGHT,
+  MAIN_VIEW_WIDTH,
+} from './consts';
 import HealthyIndexRow from './HealthyIndexRow';
-import { INodeTreeStructure } from './Types';
+import LineChartLarge from './LineChartLarge';
+import { ILineChartData, INodeTreeStructure } from './Types';
 
 // export const CHART_LABEL_WIDTH = 70;
 
@@ -9,6 +14,7 @@ const getStyles = makeStyles((theme: Theme) => ({
   root: {
     width: `${MAIN_VIEW_WIDTH}px`,
     height: '100%',
+    padding: '12px 16px',
     // boxShadow: '0 0 5px #ccc',
     fontSize: '14px',
   },
@@ -22,7 +28,7 @@ const getStyles = makeStyles((theme: Theme) => ({
     fontWeight: 500,
   },
   timeRangeTabs: {
-    fontSize: '12px'
+    fontSize: '12px',
   },
   legendContainer: {
     display: 'flex',
@@ -43,6 +49,9 @@ const getStyles = makeStyles((theme: Theme) => ({
   },
   healthyIndexLabel: {
     // width: `${CHART_LABEL_WIDTH}px`,
+
+    fontWeight: 500,
+    color: '#444',
   },
   healthyIndexRow: {
     width: `${CHART_WIDTH}px`,
@@ -50,25 +59,34 @@ const getStyles = makeStyles((theme: Theme) => ({
   },
   chartView: { width: '100%', marginTop: '30px' },
   chartItem: {
-    margin: '10px 0',
+    margin: '24px 0',
     display: 'flex',
     justifyContent: 'space-between',
     alignItems: 'flex-end',
   },
   chartLabel: {
-    // width: `${CHART_LABEL_WIDTH}px`
+    width: `50px`,
+    fontWeight: 500,
+    color: '#444',
   },
   chart: {
-    height: '100px',
+    height: `${LINE_CHART_LARGE_HEIGHT}px`,
     width: `${CHART_WIDTH}px`,
 
-    border: '1px solid brown',
+    // border: '1px solid brown',
   },
 }));
 
-const HealthyIndexOverview = ({ nodes }: { nodes: INodeTreeStructure[] }) => {
+const HealthyIndexOverview = ({
+  nodes,
+  lineChartsData,
+}: {
+  nodes: INodeTreeStructure[];
+  lineChartsData: ILineChartData[];
+}) => {
   const classes = getStyles();
   console.log('nodes', nodes);
+  console.log('lineChartsData', lineChartsData);
   return (
     <div className={classes.root}>
       <div className={classes.headerContent}>
@@ -104,18 +122,14 @@ const HealthyIndexOverview = ({ nodes }: { nodes: INodeTreeStructure[] }) => {
       </div>
       <div className={classes.chartView}>
         <div className={classes.title}>Search Query History</div>
-        <div className={classes.chartItem}>
-          <div className={classes.chartLabel}>Search Count</div>
-          <div className={classes.chart}></div>
-        </div>
-        <div className={classes.chartItem}>
-          <div className={classes.chartLabel}>Search Count</div>
-          <div className={classes.chart}></div>
-        </div>
-        <div className={classes.chartItem}>
-          <div className={classes.chartLabel}>Search Count</div>
-          <div className={classes.chart}></div>
-        </div>
+        {lineChartsData.map(chartData => (
+          <div className={classes.chartItem}>
+            <div className={classes.chartLabel}>{chartData.label}</div>
+            <div className={classes.chart}>
+              <LineChartLarge data={chartData.data} format={chartData.format} unit={chartData.unit} />
+            </div>
+          </div>
+        ))}
       </div>
     </div>
   );

+ 82 - 2
client/src/pages/systemHealthy/LineChartLarge.tsx

@@ -1,11 +1,91 @@
+import * as d3 from 'd3';
+import { useEffect } from 'react';
+import {
+  CHART_WIDTH,
+  LINE_CHART_LARGE_HEIGHT,
+  LINE_COLOR,
+  LINE_LABEL_FONT_FAMILY,
+  LINE_LABEL_FONT_SIZE,
+  LINE_LABEL_Y_PADDING,
+} from './consts';
+
 const LineChartLarge = ({
   data,
   format = d => d,
+  unit = '',
 }: {
   data: number[];
   format?: (d: any) => string;
+  unit?: string;
 }) => {
-  return <div></div>;
-};
+  const length = data.length;
+  const width = CHART_WIDTH;
+  const height = LINE_CHART_LARGE_HEIGHT - 3;
+  const fontSize = LINE_LABEL_FONT_SIZE;
+  const fontFamily = LINE_LABEL_FONT_FAMILY;
+
+  const xDomain = [0, length];
+  const xRange = [0, CHART_WIDTH];
+  let maxData = d3.max(data, d => d) as number;
+  maxData = maxData === 0 ? 1 : maxData;
+
+  const yDomain = [0, maxData * 1.1];
+  const yRange = [height, 0];
 
+  const xScale = d3.scaleLinear(xDomain, xRange);
+  const yScale = d3.scaleLinear(yDomain, yRange);
+
+  const nodes = data
+    .map((d, i) => (d >= 0 ? [xScale(i + 0.5), yScale(d)] : undefined))
+    .filter(a => a) as [number, number][];
+
+  const line = d3
+    .line()
+    .curve(d3.curveBumpX)
+    .x(d => d[0])
+    .y(d => d[1]);
+
+  return (
+    <svg
+      width={width}
+      height={height}
+      style={{ overflow: 'visible' }}
+      fontSize={fontSize}
+    >
+      <g className="x-axis">
+        <line x1={0} y1={height} x2={width} y2={height} stroke="#666" />
+      </g>
+      <g className="y-axis">
+        <line x1={0} y1={0} x2={0} y2={height} stroke="#666" />
+        <text x={-LINE_LABEL_Y_PADDING} y={height} textAnchor="end">
+          {0}
+        </text>
+        <text x={-LINE_LABEL_Y_PADDING} y={fontSize} textAnchor="end">
+          {format(maxData)}
+        </text>
+        {unit && (
+          <text
+            x={-LINE_LABEL_Y_PADDING}
+            y={fontSize * 2}
+            textAnchor="end"
+            fill={'#333'}
+            fontSize={fontSize - 2}
+          >
+            ({unit})
+          </text>
+        )}
+      </g>
+      <g className="line">
+        <path
+          d={line(nodes) as any}
+          fill="none"
+          stroke={`${LINE_COLOR}`}
+          strokeWidth={3}
+          opacity={0.8}
+          strokeLinecap="round"
+        />
+      </g>
+    </svg>
+  );
+};
 export default LineChartLarge;

+ 10 - 7
client/src/pages/systemHealthy/SystemHealthyView.tsx

@@ -31,7 +31,7 @@ const getStyles = makeStyles((theme: Theme) => ({
     height: 'fit-content',
     display: 'flex',
     flexDirection: 'column',
-    border: '1px solid red',
+    // border: '1px solid red',
   },
   mainView: {
     borderRadius: '8px',
@@ -40,14 +40,14 @@ const getStyles = makeStyles((theme: Theme) => ({
     gridTemplateColumns: '1fr auto',
     marginTop: '14px',
     height: '100%',
-    border: '1px solid green',
+    // border: '1px solid green',
   },
   detailView: {
     height: '100%',
     width: '100%',
     transition: 'all .25s',
     position: 'absolute',
-    border: '1px solid purple',
+    // border: '1px solid purple',
   },
   showDetailView: {
     top: 0,
@@ -106,16 +106,18 @@ const SystemHealthyView = () => {
     setNodes(reconNodeTree(result, threshold));
     setLineChartsData([
       {
-        label: 'TotalCount',
+        label: 'Total Count',
         data: result.totalVectorsCount,
       },
       {
-        label: 'SearchCount',
+        label: 'Search Count',
         data: result.searchVectorsCount,
       },
       {
-        label: 'SearchLatency',
+        label: 'Search Latency',
         data: result.sqLatency,
+        format: (d) => d.toFixed(0),
+        unit: 'ms'
       },
     ]);
   };
@@ -136,10 +138,11 @@ const SystemHealthyView = () => {
       <div className={classes.mainView}>
         <Topology
           nodes={nodes}
+          // nodes={nodes[2].children}
           selectedNode={selectedNode as INodeTreeStructure}
           setSelectedNode={setSelectedNode}
         />
-        <HealthyIndexOverview nodes={nodes} />
+        <HealthyIndexOverview nodes={nodes} lineChartsData={lineChartsData} />
       </div>
       <div
         className={clsx(

+ 179 - 2
client/src/pages/systemHealthy/Topology.tsx

@@ -1,5 +1,70 @@
+import { makeStyles, Theme, useTheme } from '@material-ui/core';
 import { Dispatch } from 'react';
-import { INodeTreeStructure } from './Types';
+import {
+  TOPO_HEIGHT,
+  TOPO_LINK_LENGTH,
+  TOPO_NODE_R,
+  TOPO_WIDTH,
+} from './consts';
+import { getIcon } from './getIcon';
+import { ENodeService, ENodeType, INodeTreeStructure } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    borderTopLeftRadius: '8px',
+    borderBottomLeftRadius: '8px',
+    overflow: 'auto',
+    backgroundColor: 'white',
+    // overflow: 'visible',
+  },
+  node: {
+    transition: 'all .25s',
+    cursor: 'pointer',
+    transformOrigin: '50% 50%',
+    transformBox: 'fill-box',
+
+    '& circle': {
+      transition: 'all .25s',
+    },
+
+    '& text': {
+      transition: 'all .25s',
+    },
+
+    '&:hover': {
+      transform: 'scale(1.1)',
+      filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
+      outline: 'none',
+    },
+  },
+}));
+
+const nodesLayout = (
+  nodes: INodeTreeStructure[],
+  width: number,
+  height: number
+) => {
+  const rootNode =
+    nodes.find(node => node.service === ENodeService.root) ||
+    (nodes.find(node => node.type === ENodeType.coord) as INodeTreeStructure);
+  const childrenNodes = nodes.filter(node => node !== rootNode);
+
+  const rootPos = [300, height / 2];
+  const angleStep = (2 * Math.PI) / Math.max(childrenNodes.length, 3);
+  const randomBias = angleStep * 0.4;
+  const childrenPos = childrenNodes.map((node, i) => [
+    rootPos[0] + Math.cos(angleStep * i) * TOPO_LINK_LENGTH[0],
+    rootPos[1] + Math.sin(angleStep * i) * TOPO_LINK_LENGTH[0],
+  ]);
+  const subChildrenPos = childrenNodes.map((node, i) => {
+    const angle = angleStep * i + (Math.random() - 0.5) * randomBias;
+    return [
+      rootPos[0] + Math.cos(angle) * TOPO_LINK_LENGTH[1],
+      rootPos[1] + Math.sin(angle) * TOPO_LINK_LENGTH[1],
+    ];
+  });
+  return { rootNode, childrenNodes, rootPos, childrenPos, subChildrenPos };
+};
 
 const Topology = ({
   nodes,
@@ -10,7 +75,119 @@ const Topology = ({
   selectedNode: INodeTreeStructure;
   setSelectedNode: Dispatch<INodeTreeStructure>;
 }) => {
-  return <div>Topology</div>;
+  const width = TOPO_WIDTH;
+  const height = TOPO_HEIGHT;
+
+  const classes = getStyles();
+  const theme = useTheme();
+
+  const { rootNode, childrenNodes, rootPos, childrenPos, subChildrenPos } =
+    nodesLayout(nodes, width, height);
+
+  return (
+    <div className={classes.root}>
+      <svg width={width} height={height} style={{ overflow: 'visible' }}>
+        {childrenNodes.map((node, i) => {
+          const childPos = childrenPos[i];
+          const subChildPos = subChildrenPos[i];
+          return (
+            <>
+              {node.children.length > 0 && (
+                <g className={classes.node}>
+                  <line
+                    x1={childPos[0]}
+                    y1={childPos[1]}
+                    x2={subChildPos[0]}
+                    y2={subChildPos[1]}
+                    stroke={theme.palette.primary.main}
+                  />
+                  <circle
+                    cx={subChildPos[0]}
+                    cy={subChildPos[1]}
+                    r={TOPO_NODE_R[2]}
+                    fill="#fff"
+                    stroke={theme.palette.primary.main}
+                  />
+                  {getIcon(
+                    node.children[0],
+                    theme,
+                    subChildPos[0] - 30,
+                    subChildPos[1] - 30
+                  )}
+
+                  <text
+                    fontFamily="Roboto"
+                    textAnchor="middle"
+                    fill={theme.palette.attuGrey.dark}
+                    fontSize="12"
+                    x={subChildPos[0]}
+                    y={subChildPos[1] + 50}
+                  >{`${node.children.length - 1} Node(s)`}</text>
+                </g>
+              )}
+              <g className={classes.node}>
+                <line
+                  x1={rootPos[0]}
+                  y1={rootPos[1]}
+                  x2={childPos[0]}
+                  y2={childPos[1]}
+                  stroke={theme.palette.primary.main}
+                />
+                <circle
+                  cx={childPos[0]}
+                  cy={childPos[1]}
+                  r={TOPO_NODE_R[1]}
+                  fill="#fff"
+                  stroke={theme.palette.primary.main}
+                />
+
+                {node.type === ENodeType.overview &&
+                  getIcon(node, theme, childPos[0] - 12, childPos[1] - 20)}
+
+                <text
+                  fontFamily="Roboto"
+                  textAnchor="middle"
+                  fill={theme.palette.primary.main}
+                  fontWeight="700"
+                  fontSize="12"
+                  x={childPos[0]}
+                  y={childPos[1] + (node.type === ENodeType.overview ? 18 : 6)}
+                >
+                  {node.type === ENodeType.overview
+                    ? node.label
+                    : `${node.type}-${node.label.slice(-5)}`}
+                </text>
+              </g>
+
+              <g className={classes.node}>
+                <circle
+                  cx={rootPos[0]}
+                  cy={rootPos[1]}
+                  r={TOPO_NODE_R[0]}
+                  fill="#fff"
+                  stroke={theme.palette.primary.main}
+                />
+                <text
+                  fontFamily="Roboto"
+                  textAnchor="middle"
+                  alignmentBaseline="middle"
+                  fill={theme.palette.primary.main}
+                  fontWeight="700"
+                  fontSize="24"
+                  x={`${rootPos[0]}`}
+                  y={`${rootPos[1]}`}
+                >
+                  {rootNode.type === ENodeType.overview
+                    ? rootNode.label
+                    : `${rootNode.service}`}
+                </text>
+              </g>
+            </>
+          );
+        })}
+      </svg>
+    </div>
+  );
 };
 
 export default Topology;

+ 12 - 10
client/src/pages/systemHealthy/Types.ts

@@ -6,24 +6,26 @@ export interface ITimeRangeOption {
 
 export enum ENodeType {
   overview = 0,
-  coord,
-  node,
+  coord = 'coord',
+  node = 'node',
 }
 
 export enum ENodeService {
   milvus = 0,
-  meta,
-  msgstream,
-  objstorage,
-  root,
-  query,
-  index,
-  data,
+  meta = 'Meta',
+  msgstream = 'MsgStream',
+  objstorage = 'ObjStorage',
+  root="Root",
+  query="Query",
+  index="Index",
+  data="Data",
 }
 
 export interface ILineChartData {
   label: string;
   data: number[];
+  format?: (d: number) => string;
+  unit?: string;
 }
 
 export interface INodeTreeStructure {
@@ -69,4 +71,4 @@ export interface IPrometheusAllData {
 export interface IThreshold {
   cpu: number;
   memory: number;
-}
+}

+ 11 - 0
client/src/pages/systemHealthy/consts.ts

@@ -1,5 +1,10 @@
 import { EHealthyStatus } from './Types';
 
+export const TOPO_WIDTH = 800;
+export const TOPO_HEIGHT = 600;
+export const TOPO_NODE_R = [68, 45, 30];
+export const TOPO_LINK_LENGTH = [160, 270];
+
 export const MAIN_VIEW_WIDTH = 600;
 export const CHART_WIDTH = 500;
 export const HEALTHY_INDEX_ROW_HEIGHT = 20;
@@ -10,3 +15,9 @@ export const HEALTHY_STATUS_COLORS = {
   [EHealthyStatus.warning]: '#F4DD0E',
   [EHealthyStatus.failed]: '#F16415',
 };
+
+export const LINE_CHART_LARGE_HEIGHT = 80;
+export const LINE_COLOR = '#394E97';
+export const LINE_LABEL_Y_PADDING = 6;
+export const LINE_LABEL_FONT_SIZE = 14;
+export const LINE_LABEL_FONT_FAMILY = 'Helvetica Neue';

+ 17 - 9
client/src/pages/systemHealthy/dataHandler.ts

@@ -1,5 +1,13 @@
-import * as d3 from "d3";
-import { EHealthyStatus, ENodeService, ENodeType, INodeTreeStructure, IPrometheusAllData, IPrometheusNode, IThreshold } from "./Types";
+import * as d3 from 'd3';
+import {
+  EHealthyStatus,
+  ENodeService,
+  ENodeType,
+  INodeTreeStructure,
+  IPrometheusAllData,
+  IPrometheusNode,
+  IThreshold,
+} from './Types';
 
 export const getInternalNode = (
   prometheusNodes: IPrometheusNode[],
@@ -14,7 +22,7 @@ export const getInternalNode = (
       const memory = node.memory[i];
       if (cpu === -1) return EHealthyStatus.noData;
       if (cpu === -2) return EHealthyStatus.failed;
-      console.log()
+      console.log();
       return cpu >= threshold.cpu || memory >= threshold.memory
         ? EHealthyStatus.warning
         : EHealthyStatus.healthy;
@@ -94,7 +102,7 @@ export const reconNodeTree = (
     'Query',
     threshold
   );
-  const DataNode = getInternalNode(
+  const dataNode = getInternalNode(
     prometheusData.dataNodes,
     ENodeService.data,
     'Data',
@@ -102,13 +110,13 @@ export const reconNodeTree = (
   );
 
   return [
-    metaNode,
-    msgstreamNode,
-    objstorageNode,
     rootNode,
     indexNode,
     queryNode,
-    DataNode,
+    dataNode,
+    metaNode,
+    msgstreamNode,
+    objstorageNode,
   ] as INodeTreeStructure[];
 };
 
@@ -120,4 +128,4 @@ export const getThirdPartyServiceHealthyStatus = (rate: number) => {
   return EHealthyStatus.failed;
 };
 export const rateList2healthyStatus = (rateList: number[]) =>
-  rateList.map((rate: number) => getThirdPartyServiceHealthyStatus(rate));
+  rateList.map((rate: number) => getThirdPartyServiceHealthyStatus(rate));

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 15 - 0
client/src/pages/systemHealthy/getIcon.tsx


+ 37 - 16
server/src/prometheus/prometheus.service.ts

@@ -131,14 +131,35 @@ export class PrometheusService {
     res.push(...Array(rightLossCount).fill(-2));
     return res;
   }
-  getSearchVectorsCount = (start: number, end: number, step: number) =>
-    this.getVectorsCount(searchVectorsCountMetric, start, end, step);
   getInsertVectorsCount = (start: number, end: number, step: number) =>
     this.getVectorsCount(totalVectorsCountMetric, start, end, step);
 
+  async getSearchVectorsCount(start: number, end: number, step: number) {
+    const expr = `${searchVectorsCountMetric}${PrometheusService.selector}`;
+    const result = await this.queryRange(expr, start, end, step);
+    const data = result.data.result;
+    const length = Math.floor((end - start) / step);
+
+    if (data.length === 0) return Array(length).fill(0);
+
+    const totalCount = data[0].values.map((d: any) => +d[1]);
+    const res = totalCount
+      .map((d: number, i: number) => (i > 0 ? d - totalCount[i - 1] : d))
+      .slice(1);
+
+    let leftLossCount, rightLossCount;
+    leftLossCount = Math.floor((data[0].values[0][0] * 1000 - start) / step);
+    res.unshift(...Array(leftLossCount).fill(-1));
+    rightLossCount = Math.floor(
+      (end - data[0].values[data[0].values.length - 1][0] * 1000) / step
+    );
+    res.push(...Array(rightLossCount).fill(-2));
+    return res;
+  }
+
   async getSQLatency(start: number, end: number, step: number) {
     const expr =
-      `histogram_quantile(0.99, sum by (le, query_type, pod, node_id)` +
+      `histogram_quantile(0.99, sum by (le, pod, node_id)` +
       `(rate(${sqLatencyMetric}${PrometheusService.selector}[${
         step / 1000
       }s])))`;
@@ -148,7 +169,9 @@ export class PrometheusService {
     const length = Math.floor((end - start) / step);
     if (data.length === 0) return Array(length).fill(0);
 
-    const res = data[0].values.map((d: any) => (isNaN(d[1]) ? 0 : +d[1]));
+    const res = data[0].values
+      .map((d: any) => (isNaN(d[1]) ? 0 : +d[1]))
+      // .slice(1);
     let leftLossCount, rightLossCount;
     leftLossCount = Math.floor((data[0].values[0][0] * 1000 - start) / step);
     res.unshift(...Array(leftLossCount).fill(-1));
@@ -232,7 +255,7 @@ export class PrometheusService {
       cpu.push(...Array(rightLossCount).fill(-2));
 
       const node = memoryNodes.find((data: any) => data.metric.pod === pod);
-      const memory = node.values.map((v: any) => +v[1]).slice(1);
+      const memory = node.values.map((v: any) => +v[1]);
 
       leftLossCount = Math.floor((node.values[0][0] * 1000 - start) / step);
       memory.unshift(...Array(leftLossCount).fill(-1));
@@ -240,7 +263,8 @@ export class PrometheusService {
         (end - node.values[node.values.length - 1][0] * 1000) / step
       );
       memory.push(...Array(rightLossCount).fill(-2));
-      return { type, pod, cpu, memory } as IPrometheusNode;
+
+      return { type, pod, cpu, memory: memory.slice(1) } as IPrometheusNode;
     });
 
     return nodesData;
@@ -250,16 +274,13 @@ export class PrometheusService {
     const cpuNodes = await this.getInternalNodesCPUData(start, end, step);
     const memoryNodes = await this.getInternalNodesMemoryData(start, end, step);
 
-    const [rootNodes, queryNodes, indexNodes, dataNodes] = ['root', 'query', 'index', 'data'].map(
-      (metric: string) =>
-        this.reconstructNodeData(
-          cpuNodes,
-          memoryNodes,
-          metric,
-          start,
-          end,
-          step
-        )
+    const [rootNodes, queryNodes, indexNodes, dataNodes] = [
+      'root',
+      'query',
+      'index',
+      'data',
+    ].map((metric: string) =>
+      this.reconstructNodeData(cpuNodes, memoryNodes, metric, start, end, step)
     );
     return { rootNodes, queryNodes, indexNodes, dataNodes };
   }

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio