Feroze Mohideen 2 ani în urmă
părinte
comite
d98e44a1c6

+ 4 - 8
dashboard/src/main/home/app-dashboard/expanded-app/metrics/MetricsChart.tsx

@@ -14,7 +14,7 @@ import StackedAreaChart from "./StackedAreaChart";
 import CheckboxRow from "components/form-components/CheckboxRow";
 import Loading from "components/Loading";
 import { AvailableMetrics, GenericMetricResponse, NormalizedMetricsData, NormalizedNginxStatusMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
-import { MetricNormalizer } from "../../../cluster-dashboard/expanded-chart/metrics/MetricNormalizer";
+import { MetricNormalizer } from "./utils";
 
 export const resolutions: { [range: string]: string } = {
     "1H": "1s",
@@ -173,15 +173,11 @@ const MetricsChart: React.FunctionComponent<PropsType> = ({
                         [{ results: allPodsMetrics }],
                         selectedMetric as AvailableMetrics,
                     );
-                    const allPodsAggregatedData = allPodsMetricsNormalized.getAggregatedData()
-                    if (shouldsum) {
-                        setData(allPodsAggregatedData["avg"])
-                        delete allPodsAggregatedData["avg"]
-                    }
-
+                    const [data, allPodsAggregatedData] = allPodsMetricsNormalized.getAggregatedData()
+                    setData(data)
                     setAggregatedData(allPodsAggregatedData);
 
-                    if (shouldsum && isHpaEnabled && ["cpu", "memory"].includes(selectedMetric)) {
+                    if (isHpaEnabled && ["cpu", "memory"].includes(selectedMetric)) {
                         let hpaMetricType = "cpu_hpa_threshold"
                         if (selectedMetric === "memory") {
                             hpaMetricType = "memory_hpa_threshold"

+ 218 - 0
dashboard/src/main/home/app-dashboard/expanded-app/metrics/MetricsChart2.tsx

@@ -0,0 +1,218 @@
+import React, { useContext, useEffect, useState } from "react";
+import ParentSize from "@visx/responsive/lib/components/ParentSize";
+import axios from "axios";
+import styled from "styled-components";
+
+import api from "shared/api";
+import { ChartTypeWithExtendedConfig } from "shared/types";
+import { Context } from "shared/Context";
+
+import AggregatedDataLegend from "../../../cluster-dashboard/expanded-chart/metrics/AggregatedDataLegend";
+import StatusCodeDataLegend from "./StatusCodeDataLegend";
+import AreaChart from "./AreaChart";
+import StackedAreaChart from "./StackedAreaChart";
+import CheckboxRow from "components/form-components/CheckboxRow";
+import Loading from "components/Loading";
+import { AvailableMetrics, GenericMetricResponse, NormalizedMetricsData, NormalizedNginxStatusMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
+import { MetricNormalizer } from "./utils";
+import { Metric } from "./types";
+
+export const resolutions: { [range: string]: string } = {
+    "1H": "1s",
+    "6H": "15s",
+    "1D": "15s",
+    "1M": "5h",
+};
+
+export const secondsBeforeNow: { [range: string]: number } = {
+    "1H": 60 * 60,
+    "6H": 60 * 60 * 6,
+    "1D": 60 * 60 * 24,
+    "1M": 60 * 60 * 24 * 30,
+};
+
+type PropsType = {
+    metric: Metric;
+};
+
+const MetricsChart2: React.FunctionComponent<PropsType> = ({
+    metric,
+}) => {
+
+    const renderAggregatedMetric = () => {
+
+    }
+    return (
+        <StyledMetricsChart>
+            <MetricsHeader>
+                <Flex>
+                    <MetricSelector>
+                        <MetricsLabel>{metric.label}</MetricsLabel>
+                    </MetricSelector>
+                </Flex>
+            </MetricsHeader>
+            {(data.length === 0 && Object.keys(areaData).length === 0) && isLoading === 0 && (
+                <Message>
+                    No data available yet.
+                    <Highlight color={"#8590ff"} onClick={getMetrics}>
+                        <i className="material-icons">autorenew</i>
+                        Refresh
+                    </Highlight>
+                </Message>
+            )}
+            {data.length > 0 && (
+                <>
+                    {showHpaToggle &&
+                        ["cpu", "memory"].includes(selectedMetric) && (
+                            <CheckboxRow
+                                toggle={() => setHpaEnabled((prev: any) => !prev)}
+                                checked={hpaEnabled}
+                                label="Show Autoscaling Threshold"
+                            />
+                        )}
+                    <ParentSize>
+                        {({ width, height }) => (
+                            <AreaChart
+                                dataKey={selectedMetricLabel}
+                                aggregatedData={aggregatedData}
+                                isAggregated={isAggregated}
+                                data={data}
+                                hpaData={hpaData}
+                                hpaEnabled={
+                                    hpaEnabled && ["cpu", "memory"].includes(selectedMetric)
+                                }
+                                width={width}
+                                height={height - 10}
+                                resolution={selectedRange}
+                                margin={{ top: 40, right: -40, bottom: 0, left: 50 }}
+                            />
+                        )}
+                    </ParentSize>
+                    <RowWrapper>
+                        <AggregatedDataLegend data={data} hideAvg={isAggregated} />
+                    </RowWrapper>
+                </>
+            )}
+
+            {Object.keys(areaData).length > 0 && isLoading === 0 && (
+                <>
+                    <ParentSize>
+                        {({ width, height }) => (
+                            <StackedAreaChart
+                                dataKey={selectedMetricLabel}
+                                data={areaData}
+                                width={width}
+                                height={height - 10}
+                                resolution={selectedRange}
+                                margin={{ top: 40, right: -40, bottom: 0, left: 50 }}
+                            />
+                        )}
+                    </ParentSize>
+                    <RowWrapper>
+                        <StatusCodeDataLegend />
+                    </RowWrapper>
+                </>
+            )}
+        </StyledMetricsChart>
+    );
+};
+
+export default MetricsChart2;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const Highlight = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-left: 8px;
+  color: ${(props: { color: string }) => props.color};
+  cursor: pointer;
+
+  > i {
+    font-size: 20px;
+    margin-right: 3px;
+  }
+`;
+
+const Message = styled.div`
+  display: flex;
+  height: 100%;
+  width: calc(100% - 150px);
+  align-items: center;
+  justify-content: center;
+  margin-left: 75px;
+  text-align: center;
+  color: #ffffff44;
+  font-size: 13px;
+`;
+
+const MetricsHeader = styled.div`
+  width: 100%;
+  display: flex;
+  align-items: center;
+  overflow: visible;
+  justify-content: space-between;
+`;
+
+const MetricsLabel = styled.div`
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  max-width: 200px;
+`;
+
+const MetricSelector = styled.div`
+  font-size: 13px;
+  font-weight: 500;
+  position: relative;
+  color: #ffffff;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  border-radius: 5px;
+  :hover {
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > i {
+    border-radius: 20px;
+    font-size: 20px;
+    margin-left: 10px;
+  }
+`;
+
+const RowWrapper = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: flex-end;
+`;
+
+
+const StyledMetricsChart = styled.div`
+  width: 100%;
+  min-height: 240px;
+  height: calc(100vh - 700px);
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  font-size: 13px;
+  animation: floatIn 0.3s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;

+ 128 - 79
dashboard/src/main/home/app-dashboard/expanded-app/metrics/MetricsSection.tsx

@@ -3,15 +3,18 @@ import styled from "styled-components";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { ChartTypeWithExtendedConfig } from "shared/types";
+import { ChartType } from "shared/types";
 
 import TabSelector from "components/TabSelector";
 import SelectRow from "components/form-components/SelectRow";
 import MetricsChart from "./MetricsChart";
-import { getServiceNameFromControllerName } from "./utils";
-import { Metric, MetricType } from "./types";
+import { getServiceNameFromControllerName, MetricNormalizer } from "./utils";
+import { Metric, MetricType, NginxStatusMetric } from "./types";
+import { match } from "ts-pattern";
+import { AvailableMetrics, NormalizedMetricsData } from "main/home/cluster-dashboard/expanded-chart/metrics/types";
+
 type PropsType = {
-  currentChart: ChartTypeWithExtendedConfig;
+  currentChart: ChartType;
   appName: string;
   serviceName?: string;
 };
@@ -41,7 +44,6 @@ const MetricsSection: React.FunctionComponent<PropsType> = ({
   const [ingressOptions, setIngressOptions] = useState([]);
   const [selectedIngress, setSelectedIngress] = useState(null);
   const [selectedRange, setSelectedRange] = useState("1H");
-  const [selectedMetric, setSelectedMetric] = useState("cpu");
   const [isLoading, setIsLoading] = useState(0);
   const [metrics, setMetrics] = useState<Metric[]>([]);
 
@@ -120,63 +122,13 @@ const MetricsSection: React.FunctionComponent<PropsType> = ({
   }, [currentChart, currentCluster, currentProject]);
 
   useEffect(() => {
-    getControllerPods();
     refreshMetrics();
   }, [selectedController]);
 
-  const getControllerPods = () => {
-    let selectors = [] as string[];
-    let ml =
-      selectedController?.spec?.selector?.matchLabels ||
-      selectedController?.spec?.selector;
-    let i = 1;
-    let selector = "";
-    for (var key in ml) {
-      selector += key + "=" + ml[key];
-      if (i != Object.keys(ml).length) {
-        selector += ",";
-      }
-      i += 1;
-    }
-
-    selectors.push(selector);
-
-    if (selectors[0] === "") {
+  const refreshMetrics = async () => {
+    if (currentProject?.id == null || currentCluster?.id == null) {
       return;
     }
-
-    setIsLoading((prev) => prev + 1);
-
-    api
-      .getMatchingPods(
-        "<token>",
-        {
-          namespace: selectedController?.metadata?.namespace,
-          selectors,
-        },
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        let pods = [{ value: "All", label: "All (Summed)" }] as any[];
-        res?.data?.forEach((pod: any) => {
-          let name = pod?.metadata?.name;
-          pods.push({ value: name, label: name });
-        });
-        setPods(pods);
-      })
-      .catch((err) => {
-        setCurrentError(JSON.stringify(err));
-        return;
-      })
-      .finally(() => {
-        setIsLoading((prev) => prev - 1);
-      });
-  };
-
-  const refreshMetrics = async () => {
     const newMetrics = [] as Metric[];
     const metricTypes: MetricType[] = ["cpu", "memory", "network", "nginx:status"];
 
@@ -191,11 +143,118 @@ const MetricsSection: React.FunctionComponent<PropsType> = ({
       metricTypes.push("nginx:errors");
     }
 
-    metricTypes.forEach((metricType) => {
-      if (metricType === "nginx:status") {
-      }
-    });
+    const d = new Date();
+    const end = Math.round(d.getTime() / 1000);
+    const start = end - secondsBeforeNow[selectedRange];
 
+    for (const metricType of metricTypes) {
+      const kind = metricType === "nginx:status" ? "Ingress" : selectedController?.kind
+      try {
+        const aggregatedMetricsResponse = await api.getMetrics(
+          "<token>",
+          {
+            metric: metricType,
+            shouldsum: false,
+            kind: kind,
+            name: selectedController?.metadata.name,
+            namespace: currentChart.namespace,
+            startrange: start,
+            endrange: end,
+            resolution: resolutions[selectedRange],
+            pods: [],
+          },
+          {
+            id: currentProject.id,
+            cluster_id: currentCluster.id,
+          }
+        );
+        const metricsNormalizer = new MetricNormalizer(
+          [{ results: (aggregatedMetricsResponse.data ?? []).flatMap((d: any) => d.results) }],
+          metricType,
+        );
+        if (metricType === "nginx:status") {
+          const nginxMetric: NginxStatusMetric = {
+            type: metricType,
+            label: "Nginx Status Codes",
+            areaData: metricsNormalizer.getNginxStatusData(),
+          }
+          newMetrics.push(nginxMetric)
+        } else {
+          const [data, allPodsAggregatedData] = metricsNormalizer.getAggregatedData();
+          const hpaData: NormalizedMetricsData[] = [];
+
+          if (isHpaEnabled && ["cpu", "memory"].includes(metricType)) {
+            let hpaMetricType = "cpu_hpa_threshold"
+            if (metricType === "memory") {
+              hpaMetricType = "memory_hpa_threshold"
+            }
+
+            const hpaRes = await api.getMetrics(
+              "<token>",
+              {
+                metric: hpaMetricType,
+                shouldsum: true,
+                kind: kind,
+                name: selectedController?.metadata.name,
+                namespace: currentChart.namespace,
+                startrange: start,
+                endrange: end,
+                resolution: resolutions[selectedRange],
+                pods: [],
+              },
+              {
+                id: currentProject.id,
+                cluster_id: currentCluster.id,
+              }
+            );
+
+            const autoscalingMetrics = new MetricNormalizer(hpaRes.data, hpaMetricType as AvailableMetrics);
+            hpaData.push(...autoscalingMetrics.getParsedData());
+          }
+
+          const metric: Metric = match(metricType)
+            .with("cpu", () => ({
+              type: metricType,
+              label: "CPU Utilization (vCPUs)",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .with("memory", () => ({
+              type: metricType,
+              label: "RAM Utilization (Mi)",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .with("network", () => ({
+              type: metricType,
+              label: "Network Received Bytes (Ki)",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .with("hpa_replicas", () => ({
+              type: metricType,
+              label: "Number of replicas",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .with("nginx:errors", () => ({
+              type: metricType,
+              label: "Nginx Errors",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .exhaustive();
+          newMetrics.push(metric);
+        }
+      } catch (err) {
+        console.log(err);
+      }
+    };
 
     setMetrics(newMetrics);
   }
@@ -221,24 +280,14 @@ const MetricsSection: React.FunctionComponent<PropsType> = ({
     <StyledMetricsSection>
       <MetricsHeader>
         <Flex>
-          {selectedMetric === "nginx:errors" ?
-            <SelectRow
-              displayFlex={true}
-              label="Target Ingress"
-              value={selectedIngress}
-              setActiveValue={(x: any) => setSelectedIngress(x)}
-              options={ingressOptions}
-              width="100%"
-            /> :
-            <SelectRow
-              displayFlex={true}
-              label="Service"
-              value={selectedController}
-              setActiveValue={(x: any) => setSelectedController(x)}
-              options={controllerOptions}
-              width="100%"
-            />
-          }
+          <SelectRow
+            displayFlex={true}
+            label="Service"
+            value={selectedController}
+            setActiveValue={(x: any) => setSelectedController(x)}
+            options={controllerOptions}
+            width="100%"
+          />
           <Highlight color={"#7d7d81"} onClick={() => forceUpdate()}>
             <i className="material-icons">autorenew</i>
           </Highlight>

+ 11 - 13
dashboard/src/main/home/app-dashboard/expanded-app/metrics/types.ts

@@ -1,24 +1,22 @@
-import { NormalizedMetricsData } from "main/home/cluster-dashboard/expanded-chart/metrics/types";
+import { NormalizedMetricsData, NormalizedNginxStatusMetricsData } from "main/home/cluster-dashboard/expanded-chart/metrics/types";
 
-export type Metric = CPUMetric | NginxStatusMetric | NginxErrorsMetric;
 
 export type MetricType = 'cpu' | 'memory' | 'network' | 'nginx:status' | 'nginx:errors' | 'hpa_replicas';
-interface MetricBase {
+export interface Metric {
     type: MetricType;
     label: string;
 }
-interface CPUMetric extends MetricBase {
-    type: 'cpu';
-    label: 'CPU Utilization (vCPUs)';
+export interface AggregatedMetric extends Metric {
+    data: NormalizedMetricsData[];
+    aggregatedData: Record<string, NormalizedMetricsData[]>;
+    hpaData: NormalizedMetricsData[];
 }
-interface NginxStatusMetric extends MetricBase {
-    type: 'nginx:status';
-    label: "Nginx Status Codes";
-    areaData: Record<string, NormalizedMetricsData[]>;
+export interface NginxStatusMetric extends Metric {
+    areaData: NormalizedNginxStatusMetricsData[];
 }
-interface NginxErrorsMetric extends MetricBase {
-    type: 'nginx:errors';
-    label: '5XX Error Percentage';
+
+export const isNginxMetric = (metric: Metric): metric is NginxStatusMetric => {
+    return metric.type === 'nginx:status';
 }
 
 

+ 177 - 0
dashboard/src/main/home/app-dashboard/expanded-app/metrics/utils.ts

@@ -1,3 +1,20 @@
+import * as stats from "simple-statistics";
+import _ from "lodash";
+
+import {
+    GenericMetricResponse,
+    MetricsCPUDataResponse,
+    MetricsMemoryDataResponse,
+    MetricsNetworkDataResponse,
+    MetricsNGINXErrorsDataResponse,
+    MetricsNGINXStatusDataResponse,
+    AvailableMetrics,
+    MetricsHpaReplicasDataResponse,
+    MetricsNGINXLatencyDataResponse,
+    NormalizedMetricsData,
+    NormalizedNginxStatusMetricsData,
+} from "main/home/cluster-dashboard/expanded-chart/metrics/types";
+
 // these match log colors
 export const StatusCodeDataColors: Record<string, string> = {
     "1xx": "#4B4F7C", // gray
@@ -85,3 +102,163 @@ export const getServiceNameFromControllerName = (controllerName: string, porterA
     return index !== -1 ? controllerName.substring(0, index) : controllerName;
 }
 
+/**
+ * Normalize values from the API to be readable by the AreaChart component.
+ * This class was created to reduce the amount of parsing inside the MetricsSection component
+ * and improve readability
+ */
+export class MetricNormalizer {
+    metric_results: GenericMetricResponse["results"];
+    kind: AvailableMetrics;
+
+    constructor(data: GenericMetricResponse[], kind: AvailableMetrics) {
+        if (!Array.isArray(data) || !data[0]?.results) {
+            throw new Error("Failed parsing response" + JSON.stringify(data));
+        }
+        this.metric_results = data[0].results;
+        this.kind = kind;
+    }
+
+    getParsedData(): NormalizedMetricsData[] {
+        if (this.kind.includes("cpu")) {
+            return this.parseCPUMetrics(this.metric_results);
+        }
+        if (this.kind.includes("memory")) {
+            return this.parseMemoryMetrics(this.metric_results);
+        }
+        if (this.kind.includes("network")) {
+            return this.parseNetworkMetrics(this.metric_results);
+        }
+        if (this.kind.includes("nginx:errors")) {
+            return this.parseNGINXErrorsMetrics(this.metric_results);
+        }
+        if (
+            this.kind.includes("nginx:latency") ||
+            this.kind.includes("nginx:latency-histogram")
+        ) {
+            return this.parseNGINXLatencyMetrics(this.metric_results);
+        }
+        if (this.kind.includes("hpa_replicas")) {
+            return this.parseHpaReplicaMetrics(this.metric_results);
+        }
+        return [];
+    }
+
+    getNginxStatusData(): NormalizedNginxStatusMetricsData[] {
+        if (this.kind.includes("nginx:status")) {
+            return this.parseNGINXStatusMetrics(this.metric_results);
+        }
+
+        return []
+    }
+
+    getAggregatedData(): [NormalizedMetricsData[], Record<string, NormalizedMetricsData[]>] {
+        const groupedByDate = _.groupBy(this.getParsedData(), "date");
+
+        const avg = Object.keys(groupedByDate).map((date) => {
+            const values = groupedByDate[date].map((d) => d.value);
+            return {
+                date: Number(date),
+                value: stats.mean(values),
+            };
+        });
+
+        const min = Object.keys(groupedByDate).map((date) => {
+            const values = groupedByDate[date].map((d) => d.value);
+            return {
+                date: Number(date),
+                value: stats.min(values),
+            };
+        });
+
+        const max = Object.keys(groupedByDate).map((date) => {
+            const values = groupedByDate[date].map((d) => d.value);
+            return {
+                date: Number(date),
+                value: stats.max(values),
+            };
+        });
+
+        return [avg, {
+            min,
+            max,
+        }];
+    }
+
+    private parseCPUMetrics(arr: MetricsCPUDataResponse["results"]) {
+        return arr.map((d) => {
+            return {
+                date: d.date,
+                value: parseFloat(d.cpu),
+            };
+        });
+    }
+
+    private parseMemoryMetrics(arr: MetricsMemoryDataResponse["results"]) {
+        return arr.map((d) => {
+            return {
+                date: d.date,
+                value: parseFloat(d.memory) / (1024 * 1024), // put units in Mi
+            };
+        });
+    }
+
+    private parseNetworkMetrics(arr: MetricsNetworkDataResponse["results"]) {
+        return arr.map((d) => {
+            return {
+                date: d.date,
+                value: parseFloat(d.bytes) / 1024, // put units in Ki
+            };
+        });
+    }
+
+    private parseNGINXErrorsMetrics(
+        arr: MetricsNGINXErrorsDataResponse["results"]
+    ) {
+        return arr.map((d) => {
+            return {
+                date: d.date,
+                value: parseFloat(d.error_pct),
+            };
+        });
+    }
+
+    private parseNGINXStatusMetrics(
+        arr: MetricsNGINXStatusDataResponse["results"]
+    ) {
+        return arr.map((d) => {
+            return {
+                date: d.date,
+                "1xx": parseInt(d["1xx"]),
+                "2xx": parseInt(d["2xx"]),
+                "3xx": parseInt(d["3xx"]),
+                "4xx": parseInt(d["4xx"]),
+                "5xx": parseInt(d["5xx"]),
+            };
+        });
+    }
+
+    private parseNGINXLatencyMetrics(
+        arr: MetricsNGINXLatencyDataResponse["results"]
+    ) {
+        return arr.map((d) => {
+            return {
+                date: d.date,
+                value: d.latency != "NaN" ? parseFloat(d.latency) : 0,
+            };
+        });
+    }
+
+    private parseHpaReplicaMetrics(
+        arr: MetricsHpaReplicasDataResponse["results"]
+    ) {
+        return arr.map((d) => {
+            return {
+                date: d.date,
+                value: parseInt(d.replicas),
+            };
+        });
+    }
+}
+
+