Browse Source

[attu server] range query from prometheus

Signed-off-by: min.tian <min.tian.cn@gmail.com>
min.tian 2 years ago
parent
commit
ad6514a5a6

+ 12 - 9
client/src/App.tsx

@@ -5,19 +5,22 @@ import { RootProvider } from './context/Root';
 import { NavProvider } from './context/Navigation';
 import { AuthProvider } from './context/Auth';
 import { WebSocketProvider } from './context/WebSocket';
+import { PrometheusProvider } from './context/Prometheus';
 
 function App() {
   return (
     <AuthProvider>
-      <RootProvider>
-        <WebSocketProvider>
-          <NavProvider>
-            <MuiPickersUtilsProvider utils={DayjsUtils}>
-              <Router></Router>
-            </MuiPickersUtilsProvider>
-          </NavProvider>
-        </WebSocketProvider>
-      </RootProvider>
+      <PrometheusProvider>
+        <RootProvider>
+          <WebSocketProvider>
+            <NavProvider>
+              <MuiPickersUtilsProvider utils={DayjsUtils}>
+                <Router></Router>
+              </MuiPickersUtilsProvider>
+            </NavProvider>
+          </WebSocketProvider>
+        </RootProvider>
+      </PrometheusProvider>
     </AuthProvider>
   );
 }

+ 9 - 2
client/src/components/customRadio/CustomRadio.tsx

@@ -3,9 +3,10 @@ import { FormGroup, FormControlLabel, Switch } from '@material-ui/core';
 
 export const CustomRadio = (props: {
   label: string;
+  defaultChecked?: boolean;
   handleChange: (checked: boolean) => void;
 }) => {
-  const { label, handleChange } = props;
+  const { label, defaultChecked = false, handleChange } = props;
   const onChange = (
     e: React.ChangeEvent<HTMLInputElement>,
     checked: boolean
@@ -15,7 +16,13 @@ export const CustomRadio = (props: {
   return (
     <FormGroup>
       <FormControlLabel
-        control={<Switch onChange={onChange} color="primary" />}
+        control={
+          <Switch
+            defaultChecked={defaultChecked}
+            onChange={onChange}
+            color="primary"
+          />
+        }
         label={label}
       />
     </FormGroup>

+ 4 - 1
client/src/consts/Localstorage.ts

@@ -1,4 +1,7 @@
 export const SESSION = 'CLOUD_SESSION';
 export const MILVUS_ADDRESS = 'milvus-address';
 export const LAST_TIME_ADDRESS = 'last-time-address';
-
+export const LAST_TIME_HAS_PROMETHEUS = 'last-time-has-prometheus';
+export const LAST_TIME_PROMETHEUS_ADDRESS = 'last-time-prometheus-address';
+export const LAST_TIME_PROMETHEUS_INSTANCE = 'last-time-prometheus-instance';
+export const LAST_TIME_PROMETHEUS_NAMESPACE = 'last-time-prometheus-namespace';

+ 1 - 0
client/src/consts/Milvus.tsx

@@ -217,6 +217,7 @@ export enum LOADING_STATE {
 export const DEFAULT_VECTORS = 100000;
 export const DEFAULT_SEFMENT_FILE_SIZE = 1024;
 export const DEFAULT_MILVUS_PORT = 19530;
+export const DEFAULT_PROMETHEUS_PORT = 9090;
 
 export enum MILVUS_NODE_TYPE {
   ROOTCOORD = 'rootcoord',

+ 94 - 0
client/src/context/Prometheus.tsx

@@ -0,0 +1,94 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { PrometheusContextType } from './Types';
+import { authContext } from '../context/Auth';
+import {
+  LAST_TIME_HAS_PROMETHEUS,
+  LAST_TIME_PROMETHEUS_ADDRESS,
+  LAST_TIME_PROMETHEUS_INSTANCE,
+  LAST_TIME_PROMETHEUS_NAMESPACE,
+} from '../consts/Localstorage';
+import { formatPrometheusAddress } from '../utils/Format';
+import { PrometheusHttp } from '../http/Prometheus';
+
+export const prometheusContext = createContext<PrometheusContextType>({
+  hasPrometheus: false,
+  setHasPrometheus: () => {},
+  isPrometheusReady: false,
+  prometheusAddress: '',
+  prometheusInstance: '',
+  prometheusNamespace: '',
+  setPrometheusAddress: () => {},
+  setPrometheusInstance: () => {},
+  setPrometheusNamespace: () => {},
+});
+
+const { Provider } = prometheusContext;
+export const PrometheusProvider = (props: { children: React.ReactNode }) => {
+  const { isAuth } = useContext(authContext);
+
+  const [hasPrometheus, setHasPrometheus] = useState(
+    !!window.localStorage.getItem(LAST_TIME_HAS_PROMETHEUS)
+  );
+  const [prometheusAddress, setPrometheusAddress] = useState(
+    window.localStorage.getItem(LAST_TIME_PROMETHEUS_ADDRESS) || ''
+  );
+  const [prometheusInstance, setPrometheusInstance] = useState(
+    window.localStorage.getItem(LAST_TIME_PROMETHEUS_INSTANCE) || ''
+  );
+  const [prometheusNamespace, setPrometheusNamespace] = useState(
+    window.localStorage.getItem(LAST_TIME_PROMETHEUS_NAMESPACE) || ''
+  );
+
+  const [isPrometheusReady, setIsPrometheusReady] = useState(false);
+
+  useEffect(() => {
+    if (!isAuth) return;
+    if (hasPrometheus) {
+      const prometheusAddressformat =
+        formatPrometheusAddress(prometheusAddress);
+      PrometheusHttp.setPrometheus({
+        prometheusAddress: prometheusAddressformat,
+        prometheusInstance,
+        prometheusNamespace,
+      }).then(({ isReady }: { isReady: boolean }) => {
+        console.log('prometheus is ready?', isReady);
+        if (isReady) {
+          window.localStorage.setItem(LAST_TIME_HAS_PROMETHEUS, 'true');
+          window.localStorage.setItem(
+            LAST_TIME_PROMETHEUS_ADDRESS,
+            prometheusAddress
+          );
+          window.localStorage.setItem(
+            LAST_TIME_PROMETHEUS_INSTANCE,
+            prometheusInstance
+          );
+          window.localStorage.setItem(
+            LAST_TIME_PROMETHEUS_NAMESPACE,
+            prometheusNamespace
+          );
+        }
+        setIsPrometheusReady(isReady);
+      });
+    } else {
+      setIsPrometheusReady(false);
+    }
+  }, [isAuth, setIsPrometheusReady]);
+
+  return (
+    <Provider
+      value={{
+        hasPrometheus,
+        setHasPrometheus,
+        isPrometheusReady,
+        prometheusAddress,
+        prometheusInstance,
+        prometheusNamespace,
+        setPrometheusAddress,
+        setPrometheusInstance,
+        setPrometheusNamespace,
+      }}
+    >
+      {props.children}
+    </Provider>
+  );
+};

+ 12 - 0
client/src/context/Types.ts

@@ -61,6 +61,18 @@ export type AuthContextType = {
   setIsAuth: Dispatch<SetStateAction<boolean>>;
 };
 
+export type PrometheusContextType = {
+  hasPrometheus: boolean;
+  setHasPrometheus: Dispatch<SetStateAction<boolean>>;
+  isPrometheusReady: boolean;
+  prometheusAddress: string;
+  prometheusInstance: string;
+  prometheusNamespace: string;
+  setPrometheusAddress: Dispatch<SetStateAction<string>>;
+  setPrometheusInstance: Dispatch<SetStateAction<string>>;
+  setPrometheusNamespace: Dispatch<SetStateAction<string>>;
+};
+
 export type NavContextType = {
   navInfo: NavInfo;
   setNavInfo: (param: NavInfo) => void;

+ 41 - 0
client/src/http/Prometheus.ts

@@ -0,0 +1,41 @@
+import BaseModel from './BaseModel';
+
+export class PrometheusHttp extends BaseModel {
+  static SET_PROMETHEUS_URL = '/prometheus/setPrometheus';
+  static GET_MILVUS_HEALTHY_DATA_URL = '/prometheus/getMilvusHealthyData';
+
+  constructor(props: {}) {
+    super(props);
+    Object.assign(this, props);
+  }
+
+  static setPrometheus({
+    prometheusAddress,
+    prometheusInstance,
+    prometheusNamespace,
+  }: {
+    prometheusAddress: string;
+    prometheusInstance: string;
+    prometheusNamespace: string;
+  }) {
+    return super.search({
+      path: PrometheusHttp.SET_PROMETHEUS_URL,
+      params: { prometheusAddress, prometheusInstance, prometheusNamespace },
+    });
+  }
+
+  static getHealthyData({
+    start,
+    end,
+    step,
+  }: {
+    start: number;
+    end: number;
+    step: string;
+  }) {
+    return super.search({
+      path: PrometheusHttp.GET_MILVUS_HEALTHY_DATA_URL,
+      params: { start, end, step },
+    });
+  }
+}

+ 4 - 0
client/src/i18n/cn/common.ts

@@ -6,6 +6,10 @@ const commonTrans = {
     username: 'Username',
     password: 'Password',
     optional: '(optional)',
+    prometheus: 'Prometheus',
+    prometheusAddress: 'Prometheus Address',
+    prometheusInstance: 'Prometheus Instance',
+    prometheusNamespace: 'Prometheus Namespace',
     ssl: 'SSL',
   },
   status: {

+ 4 - 0
client/src/i18n/en/common.ts

@@ -6,6 +6,10 @@ const commonTrans = {
     username: 'Username',
     password: 'Password',
     optional: '(optional)',
+    prometheus: 'Prometheus',
+    prometheusAddress: 'Prometheus Address',
+    prometheusInstance: 'Prometheus Instance',
+    prometheusNamespace: 'Prometheus Namespace',
     ssl: 'SSL',
   },
   status: {

+ 79 - 3
client/src/pages/connect/AuthForm.tsx

@@ -9,14 +9,23 @@ import { ITextfieldConfig } from '../../components/customInput/Types';
 import { useFormValidation } from '../../hooks/Form';
 import { formatForm } from '../../utils/Form';
 import { MilvusHttp } from '../../http/Milvus';
-import { formatAddress } from '../../utils/Format';
+import { PrometheusHttp } from '../../http/Prometheus';
+import { formatAddress, formatPrometheusAddress } from '../../utils/Format';
 import { useNavigate } from 'react-router-dom';
 import { rootContext } from '../../context/Root';
 import { authContext } from '../../context/Auth';
-import { MILVUS_ADDRESS, LAST_TIME_ADDRESS } from '../../consts/Localstorage';
+import {
+  MILVUS_ADDRESS,
+  LAST_TIME_ADDRESS,
+  LAST_TIME_HAS_PROMETHEUS,
+  LAST_TIME_PROMETHEUS_ADDRESS,
+  LAST_TIME_PROMETHEUS_INSTANCE,
+  LAST_TIME_PROMETHEUS_NAMESPACE,
+} from '../../consts/Localstorage';
 import { CODE_STATUS } from '../../consts/Http';
 import { MILVUS_URL } from '../../consts/Milvus';
 import { CustomRadio } from '../../components/customRadio/CustomRadio';
+import { prometheusContext } from '../../context/Prometheus';
 
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
@@ -45,7 +54,7 @@ const useStyles = makeStyles((theme: Theme) => ({
     display: 'block',
   },
   input: {
-    margin: theme.spacing(3, 0, 0.5),
+    margin: theme.spacing(0.5, 0, 0),
   },
   sslWrapper: {
     display: 'flex',
@@ -133,6 +142,56 @@ export const AuthForm = (props: any) => {
     ];
   }, [form, attuTrans, warningTrans, classes.input]);
 
+  const {
+    hasPrometheus,
+    setHasPrometheus,
+    prometheusAddress,
+    prometheusInstance,
+    prometheusNamespace,
+    setPrometheusAddress,
+    setPrometheusInstance,
+    setPrometheusNamespace,
+  } = useContext(prometheusContext);
+
+  const prometheusConfigs: ITextfieldConfig[] = useMemo(
+    () => [
+      {
+        label: `${attuTrans.prometheusAddress}`,
+        key: 'prometheus_address',
+        onChange: setPrometheusAddress,
+        variant: 'filled',
+        className: classes.input,
+        placeholder: attuTrans.prometheusAddress,
+        fullWidth: true,
+
+        defaultValue: prometheusAddress,
+      },
+      {
+        label: `${attuTrans.prometheusInstance}`,
+        key: 'prometheus_instance',
+        onChange: setPrometheusInstance,
+        variant: 'filled',
+        className: classes.input,
+        placeholder: attuTrans.prometheusInstance,
+        fullWidth: true,
+
+        defaultValue: prometheusInstance,
+      },
+      {
+        label: `${attuTrans.prometheusNamespace} ${attuTrans.optional}`,
+        key: 'prometheus_namespace',
+        onChange: setPrometheusNamespace,
+        variant: 'filled',
+        className: classes.input,
+        placeholder: attuTrans.prometheusNamespace,
+        fullWidth: true,
+
+        defaultValue: prometheusNamespace,
+      },
+    ],
+    []
+  );
+
   const handleConnect = async () => {
     const address = formatAddress(form.address);
     try {
@@ -184,6 +243,23 @@ export const AuthForm = (props: any) => {
             handleChange={(val: boolean) => handleInputChange('ssl', val)}
           />
         </div>
+        <div className={classes.sslWrapper}>
+          <CustomRadio
+            defaultChecked={hasPrometheus}
+            label={attuTrans.prometheus}
+            handleChange={setHasPrometheus}
+          />
+        </div>
+        {hasPrometheus &&
+          prometheusConfigs.map(v => (
+            <CustomInput
+              type="text"
+              textConfig={v}
+              checkValid={checkIsValid}
+              validInfo={validation}
+              key={v.label}
+            />
+          ))}
         <CustomButton type="submit" variant="contained" disabled={btnDisabled}>
           {btnTrans('connect')}
         </CustomButton>

+ 6 - 1
client/src/pages/index.tsx

@@ -10,6 +10,7 @@ import icons from '../components/icons/Icons';
 import { authContext } from '../context/Auth';
 import { rootContext } from '../context/Root';
 import Overview from '../pages/overview/Overview';
+import { prometheusContext } from '../context/Prometheus';
 
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
@@ -45,6 +46,7 @@ const useStyles = makeStyles((theme: Theme) =>
 function Index() {
   const navigate = useNavigate();
   const { isAuth } = useContext(authContext);
+  const { isPrometheusReady } = useContext(prometheusContext);
   const { versionInfo } = useContext(rootContext);
   const { t: navTrans } = useTranslation('nav');
   const classes = useStyles();
@@ -91,7 +93,10 @@ function Index() {
     {
       icon: icons.navSystem,
       label: navTrans('system'),
-      onClick: () => navigate('/system'),
+      onClick: () =>
+        isPrometheusReady
+          ? navigate('/system_healthy')
+          : navigate('/system'),
       iconActiveClass: 'normal',
       iconNormalClass: 'active',
     },

+ 1 - 0
client/src/pages/system/SystemView.tsx

@@ -94,6 +94,7 @@ const SystemView: any = () => {
     async function fetchData() {
       const res = await MilvusHttp.getMetrics();
       setData(parseJson(res));
+      console.log(parseJson(res))
     }
     fetchData();
   }, []);

+ 62 - 0
client/src/pages/systemHealthy/SystemHealthyView.tsx

@@ -0,0 +1,62 @@
+import { useEffect, useState } from 'react';
+import { useNavigationHook } from '../../hooks/Navigation';
+import { useInterval } from '../../hooks/SystemView';
+import { PrometheusHttp } from '../../http/Prometheus';
+import { ALL_ROUTER_TYPES } from '../../router/Types';
+
+interface ITimeRangeOption {
+  label: string;
+  value: number;
+  step: string;
+}
+
+const SystemHealthyView = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.SYSTEM);
+
+  const INTERVAL = 60000;
+  const timeRangeOptions: ITimeRangeOption[] = [
+    {
+      label: '1h',
+      value: 60 * 60 * 1000,
+      step: '3m',
+    },
+    {
+      label: '24h',
+      value: 24 * 60 * 60 * 1000,
+      step: '60m',
+    },
+    {
+      label: '7d',
+      value: 7 * 24 * 60 * 60 * 1000,
+      step: '8h',
+    },
+  ];
+  const [timeRange, setTimeRange] = useState<ITimeRangeOption>(
+    timeRangeOptions[1]
+  );
+  const [nodes, setNodes] = useState<any>();
+
+  const updateData = async () => {
+    const curT = new Date().getTime();
+    const result: any = await PrometheusHttp.getHealthyData({
+      start: curT - timeRange.value,
+      end: curT,
+      step: timeRange.step,
+    });
+    console.log(result);
+    setNodes(result.data);
+  };
+
+  useEffect(() => {
+    updateData();
+  }, []);
+
+  useInterval(() => {
+    console.log('interval');
+    updateData();
+  }, INTERVAL);
+
+  return <div></div>;
+};
+
+export default SystemHealthyView;

+ 5 - 0
client/src/router/Router.tsx

@@ -6,6 +6,7 @@ import Users from '../pages/user/User';
 import Index from '../pages/index';
 import Search from '../pages/search/VectorSearch';
 import System from '../pages/system/SystemView';
+import SystemHealthy from '../pages/systemHealthy/SystemHealthyView';
 
 const router = createHashRouter([
   {
@@ -32,6 +33,10 @@ const router = createHashRouter([
         path: '/system',
         element: <System />,
       },
+      {
+        path: '/system_healthy',
+        element: <SystemHealthy />,
+      },
     ],
   },
   { path: '/connect', element: <Connect /> },

+ 8 - 1
client/src/utils/Format.ts

@@ -1,5 +1,5 @@
 import { BYTE_UNITS } from '../consts/Util';
-import { DEFAULT_MILVUS_PORT } from '../consts/Milvus';
+import { DEFAULT_MILVUS_PORT, DEFAULT_PROMETHEUS_PORT } from '../consts/Milvus';
 import {
   CreateFieldType,
   DataTypeEnum,
@@ -148,6 +148,13 @@ export const formatAddress = (address: string): string => {
   return ip.includes(':') ? ip : `${ip}:${DEFAULT_MILVUS_PORT}`;
 };
 
+// Trim the prometheus address
+export const formatPrometheusAddress = (address: string): string => {
+  // remove http or https prefix from address
+  const ip = address.replace(/(http|https):\/\//, '');
+  return ip.includes(':') ? ip : `${ip}:${DEFAULT_PROMETHEUS_PORT}`;
+};
+
 export const formatByteSize = (
   size: number,
   capacityTrans: { [key in string]: string }

+ 1 - 0
server/package.json

@@ -13,6 +13,7 @@
   },
   "dependencies": {
     "@zilliz/milvus2-sdk-node": "^2.2.1",
+    "axios": "^1.3.2",
     "chalk": "^4.1.2",
     "class-sanitizer": "^1.0.1",
     "class-transformer": "^0.4.0",

+ 2 - 0
server/src/app.ts

@@ -13,6 +13,7 @@ import { router as partitionsRouter } from './partitions';
 import { router as schemaRouter } from './schema';
 import { router as cronsRouter } from './crons';
 import { router as userRouter } from './users';
+import { router as prometheusRouter } from './prometheus';
 import { pubSub } from './events';
 import {
   TransformResMiddlerware,
@@ -41,6 +42,7 @@ router.use('/partitions', partitionsRouter);
 router.use('/schema', schemaRouter);
 router.use('/crons', cronsRouter);
 router.use('/users', userRouter);
+router.use('/prometheus', prometheusRouter);
 router.get('/healthy', (req, res, next) => {
   res.json({ status: 200 });
   next();

+ 0 - 0
server/src/prometheus/dto.ts


+ 6 - 0
server/src/prometheus/index.ts

@@ -0,0 +1,6 @@
+import { PrometheusController } from './prometheus.controller';
+
+const prometheusManager = new PrometheusController();
+const router = prometheusManager.generateRoutes();
+
+export { router };

+ 46 - 0
server/src/prometheus/prometheus.controller.ts

@@ -0,0 +1,46 @@
+import { NextFunction, Request, Response, Router } from 'express';
+import { PrometheusService } from './prometheus.service';
+
+export class PrometheusController {
+  private router: Router;
+  private prometheusService: PrometheusService;
+
+  constructor() {
+    this.prometheusService = new PrometheusService();
+    this.router = Router();
+  }
+
+  generateRoutes() {
+    this.router.get('/setPrometheus', this.setPrometheus.bind(this));
+    this.router.get(
+      '/getMilvusHealthyData',
+      this.getMilvusHealthyData.bind(this)
+    );
+
+    return this.router;
+  }
+
+  async setPrometheus(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.prometheusService.setPrometheus(
+        req.query as any
+      );
+      res.send(result);
+    } catch (err) {
+      console.error(err);
+      next(err);
+    }
+  }
+
+  async getMilvusHealthyData(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.prometheusService.getMilvusHealthyData(
+        req.query as any
+      );
+      res.send(result);
+    } catch (err) {
+      console.error(err);
+      next(err);
+    }
+  }
+}

+ 184 - 0
server/src/prometheus/prometheus.service.ts

@@ -0,0 +1,184 @@
+import axios from 'axios';
+
+interface IPrometheusNode {
+  label: string;
+  cpu: number[];
+  memory: number[];
+}
+
+interface IPrometheusAllData {
+  totalVectorsCount: number[];
+  searchVectorsCount: number[];
+  searchFailedVectorsCount: number[];
+  sqLatency: number[];
+
+  metaHealthy: number[];
+  msgHealthy: number[];
+  objectHealthy: number[];
+
+  queryNodes: IPrometheusNode[];
+  indexNodes: IPrometheusNode[];
+  dataNodes: IPrometheusNode[];
+}
+
+const metaMetric = 'milvus_meta_op_count';
+const msgstreamMetric = 'milvus_msgstream_op_count';
+const objstorageMetric = 'milvus_storage_op_count';
+
+export class PrometheusService {
+  static address: string = '';
+  static instance: string = '';
+  static namespace: string = '';
+  static isReady: boolean = false;
+
+  static get url() {
+    return `http://${PrometheusService.address}`;
+  }
+  static get selector() {
+    return (
+      '{' +
+      `app_kubernetes_io_instance="${PrometheusService.instance}"` +
+      `,namespace="${PrometheusService.namespace}"` +
+      '}'
+    );
+  }
+
+  constructor() {
+    // todo
+  }
+
+  async setPrometheus({
+    prometheusAddress,
+    prometheusInstance,
+    prometheusNamespace,
+  }: {
+    prometheusAddress: string;
+    prometheusInstance: string;
+    prometheusNamespace: string;
+  }) {
+    PrometheusService.isReady = await this.checkPrometheus(prometheusAddress);
+    if (PrometheusService.isReady) {
+      PrometheusService.address = prometheusAddress;
+      PrometheusService.instance = prometheusInstance;
+      PrometheusService.namespace = prometheusNamespace;
+    }
+
+    return {
+      isReady: PrometheusService.isReady,
+    };
+  }
+
+  async checkPrometheus(prometheusAddress: string) {
+    const result = await axios
+      .get(`http://${prometheusAddress}/-/ready`)
+      .then(res => res.status)
+      .catch(err => {
+        console.log(err);
+      });
+    return result === 200;
+  }
+
+  async queryRange(expr: string, start: number, end: number, step: string) {
+    const url =
+      PrometheusService.url +
+      '/api/v1/query_range?query=' +
+      expr +
+      `&start=${new Date(+start).toISOString()}` +
+      `&end=${new Date(+end).toISOString()}` +
+      `&step=${step}`;
+    console.log(url);
+    const result = await axios
+      .get(url)
+      .then(res => res.data)
+      .catch(err => {
+        console.log(err);
+        return { status: 'failed' };
+      });
+    return result;
+  }
+
+  getSearchVectorsCount() {
+    return;
+  }
+
+  getInsertVectorsCount() {
+    return;
+  }
+
+  getSQLatency() {
+    return;
+  }
+
+  async getMetaServiceHealthyStatus(start: number, end: number, step: string) {}
+
+  async getThirdPartyServiceHealthStatus(
+    metricName: string,
+    start: number,
+    end: number,
+    step: string
+  ) {
+    const expr = `sum by (status) (${metricName}${PrometheusService.selector})`;
+    const result = await this.queryRange(expr, start, end, step);
+    const totalCount = result.data.result
+      .find((d: any) => d.metric.status === 'total')
+      .values.map((d: any) => +d[1]);
+    const totalSlices = totalCount
+      .map((d: number, i: number) => (i > 0 ? d - totalCount[i - 1] : d))
+      .slice(1);
+    const successCount = result.data.result
+      .find((d: any) => d.metric.status === 'success')
+      .values.map((d: any) => +d[1]);
+    const successSlices = successCount
+      .map((d: number, i: number) => (i > 0 ? d - successCount[i - 1] : d))
+      .slice(1);
+    return totalSlices.map((d: number, i: number) =>
+      d === 0 ? 1 : successSlices[i] / d
+    );
+  }
+
+  getMessageServiceHealthyStatus() {
+    return;
+  }
+
+  getObjectServiceHealthyStatus() {
+    return;
+  }
+
+  getInternalNodeCPUData() {
+    return;
+  }
+
+  getInternalNodeMemoryData() {
+    return;
+  }
+
+  async getMilvusHealthyData({
+    start,
+    end,
+    step,
+  }: {
+    start: number;
+    end: number;
+    step: string;
+  }) {
+    const meta = await this.getThirdPartyServiceHealthStatus(
+      metaMetric,
+      start,
+      end,
+      step
+    );
+    const msgstream = await this.getThirdPartyServiceHealthStatus(
+      msgstreamMetric,
+      start,
+      end,
+      step
+    );
+    const objstorage = await this.getThirdPartyServiceHealthStatus(
+      objstorageMetric,
+      start,
+      end,
+      step
+    );
+    return { meta, msgstream, objstorage } as any;
+  }
+}

+ 19 - 0
server/yarn.lock

@@ -1390,6 +1390,15 @@ at-least-node@^1.0.0:
   resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
   integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
 
+axios@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.npmmirror.com/axios/-/axios-1.3.2.tgz#7ac517f0fa3ec46e0e636223fd973713a09c72b3"
+  integrity sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==
+  dependencies:
+    follow-redirects "^1.15.0"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
 babel-jest@^27.3.1:
   version "27.3.1"
   resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.3.1.tgz#0636a3404c68e07001e434ac4956d82da8a80022"
@@ -2580,6 +2589,11 @@ find-up@^4.0.0, find-up@^4.1.0:
     locate-path "^5.0.0"
     path-exists "^4.0.0"
 
+follow-redirects@^1.15.0:
+  version "1.15.2"
+  resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+  integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
 form-data@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
@@ -4360,6 +4374,11 @@ proxy-addr@~2.0.5:
     forwarded "0.2.0"
     ipaddr.js "1.9.1"
 
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
 psl@^1.1.33:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"