Jelajahi Sumber

refactor: use a single query for showing the metrics

Jose Diaz-Gonzalez 2 tahun lalu
induk
melakukan
0ecf2915f8

+ 27 - 40
dashboard/src/main/home/app-dashboard/expanded-app/metrics/MetricsChart.tsx

@@ -13,7 +13,7 @@ import AreaChart from "./AreaChart";
 import StackedAreaChart from "./StackedAreaChart";
 import CheckboxRow from "components/form-components/CheckboxRow";
 import Loading from "components/Loading";
-import { AvailableMetrics, GenericMetricResponse, NormalizedMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
+import { AvailableMetrics, GenericMetricResponse, NormalizedMetricsData, NormalizedNginxStatusMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
 import { MetricNormalizer } from "../../../cluster-dashboard/expanded-chart/metrics/MetricNormalizer";
 
 export const resolutions: { [range: string]: string } = {
@@ -56,7 +56,7 @@ const MetricsChart: React.FunctionComponent<PropsType> = ({
         Record<string, NormalizedMetricsData[]>
     >({});
     const [data, setData] = useState<NormalizedMetricsData[]>([]);
-    const [areaData, setAreaData] = useState<Record<string, NormalizedMetricsData[]>>({});
+    const [areaData, setAreaData] = useState<NormalizedNginxStatusMetricsData[]>([]);
     const [hpaData, setHpaData] = useState<NormalizedMetricsData[]>([]);
     const [hpaEnabled, setHpaEnabled] = useState(false);
     const [showHpaToggle, setShowHpaToggle] = useState(false);
@@ -260,45 +260,31 @@ const MetricsChart: React.FunctionComponent<PropsType> = ({
 
         try {
             setIsLoading((prev) => prev + 1);
-            for (let i = 1; i <= 5; i++) {
-                requests.push(api.getMetrics(
-                    "<token>",
-                    {
-                        metric: selectedMetric,
-                        shouldsum: false,
-                        kind: kind,
-                        name: selectedController?.metadata.name,
-                        namespace: namespace,
-                        startrange: start,
-                        endrange: end,
-                        resolution: resolutions[selectedRange],
-                        pods: [],
-                        nginx_status_level: i,
-                    },
-                    {
-                        id: currentProject.id,
-                        cluster_id: currentCluster.id,
-                    }
-                ));
-            }
-            axios
-                .all(requests)
-                .then((responses) => {
-                    let aggregatedMetrics: Record<string, NormalizedMetricsData[]> = {};
-                    for (let i = 0; i <= 4; i++) {
-                        const metrics = new MetricNormalizer(
-                            responses[i].data,
-                            selectedMetric as AvailableMetrics
-                        );
-                        const metrixIndex = `${i+1}xx`;
-                        aggregatedMetrics[metrixIndex] = metrics.getParsedData()
-                    }
+            const response = await api.getMetrics(
+                "<token>",
+                {
+                    metric: selectedMetric,
+                    shouldsum: false,
+                    kind: kind,
+                    name: selectedController?.metadata.name,
+                    namespace: namespace,
+                    startrange: start,
+                    endrange: end,
+                    resolution: resolutions[selectedRange],
+                    pods: [],
+                },
+                {
+                    id: currentProject.id,
+                    cluster_id: currentCluster.id,
+                }
+            );
+            let aggregatedMetrics: Record<string, NormalizedMetricsData[]> = {};
+            const metrics = new MetricNormalizer(
+                response.data,
+                selectedMetric as AvailableMetrics
+            );
 
-                    setAreaData(aggregatedMetrics);
-                })
-                .catch(error => {
-                    setCurrentError(JSON.stringify(error));
-                })
+            setAreaData(metrics.getNginxStatusData());
         } catch (error) {
             setCurrentError(JSON.stringify(error));
         } finally {
@@ -389,6 +375,7 @@ const MetricsChart: React.FunctionComponent<PropsType> = ({
                     <ParentSize>
                         {({ width, height }) => (
                             <StackedAreaChart
+                                dataKey={selectedMetricLabel}
                                 data={areaData}
                                 width={width}
                                 height={height - 10}

+ 43 - 25
dashboard/src/main/home/app-dashboard/expanded-app/metrics/StackedAreaChart.tsx

@@ -1,23 +1,28 @@
 import _ from "lodash";
-import React, { useMemo, useRef } from "react";
+import React, { useCallback, useMemo, useRef } from "react";
+import styled from "styled-components";
 import { AreaSeries, AreaStack, Tooltip, XYChart } from "@visx/xychart";
 import { AxisBottom, AxisLeft } from "@visx/axis"; 
-import { scaleLinear, scaleTime } from "@visx/scale";
 import { curveMonotoneX as visxCurve } from "@visx/curve";
+import { localPoint } from "@visx/event";
+import { GridColumns, GridRows } from "@visx/grid";
+import { scaleLinear, scaleTime } from "@visx/scale";
+import { Bar, Line } from "@visx/shape";
+import { defaultStyles, TooltipWithBounds, useTooltip } from "@visx/tooltip";
 import { bisector, extent, max } from "d3-array";
 import { timeFormat } from "d3-time-format";
-import { GridColumns, GridRows } from "@visx/grid";
 
 import { default as areaTheme } from "./themes/area";
-import { NormalizedMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
+import { NormalizedNginxStatusMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
+import { StatusCodeDataColors } from "./utils";
+
+var globalData: NormalizedNginxStatusMetricsData[];
 
 export const background = "#3b697800";
 export const background2 = "#20405100";
 export const accentColor = "#949eff";
 export const accentColorDark = "#949eff";
 
-type StatusCode = "1xx" | "2xx" | "3xx" | "4xx" | "5xx";
-
 // util
 const formatDate = timeFormat("%H:%M:%S %b %d, '%y");
 
@@ -36,17 +41,28 @@ const formats: { [range: string]: (date: Date) => string } = {
 };
 
 // accessors
-const getDate = (d: NormalizedMetricsData) => new Date(d.date * 1000);
-const getDateAsString = (d: NormalizedMetricsData) => formatDate(getDate(d));
-const getValue = (d: NormalizedMetricsData) =>
-    d?.value && Number(d.value?.toFixed(4));
+const getDate = (d: NormalizedNginxStatusMetricsData) => new Date(d.date * 1000);
+const getDateAsString = (d: NormalizedNginxStatusMetricsData) => formatDate(getDate(d));
+const getStatusValue = (d: NormalizedNginxStatusMetricsData, level: string) =>{
+    const statusLevel = level as keyof NormalizedNginxStatusMetricsData;
+    return d?[statusLevel] && Number(d[statusLevel]?.toFixed(4)) : 0;
+}
+const get1xxValue = (d: NormalizedNginxStatusMetricsData) => getStatusValue(d, "1xx");
+const get2xxValue = (d: NormalizedNginxStatusMetricsData) => getStatusValue(d, "2xx");
+const get3xxValue = (d: NormalizedNginxStatusMetricsData) => getStatusValue(d, "3xx");
+const get4xxValue = (d: NormalizedNginxStatusMetricsData) => getStatusValue(d, "4xx");
+const get5xxValue = (d: NormalizedNginxStatusMetricsData) => getStatusValue(d, "5xx");
 
-const bisectDate = bisector<NormalizedMetricsData, Date>(
+const getMaxValue = (d: NormalizedNginxStatusMetricsData) =>
+    max([get1xxValue(d), get2xxValue(d), get3xxValue(d), get4xxValue(d), get5xxValue(d)]) || 0;
+
+const bisectDate = bisector<NormalizedNginxStatusMetricsData, Date>(
     (d) => new Date(d.date * 1000)
 ).left;
 
 export type StackedAreaChartProps = {
-    data: Record<string, NormalizedMetricsData[]>;
+    data: NormalizedNginxStatusMetricsData[];
+    dataKey: string;
     resolution: string;
     width: number;
     height: number;
@@ -55,6 +71,7 @@ export type StackedAreaChartProps = {
 
 const StackedAreaChart: React.FunctionComponent<StackedAreaChartProps> = ({
     data,
+    dataKey,
     resolution,
     width,
     height,
@@ -72,12 +89,13 @@ const StackedAreaChart: React.FunctionComponent<StackedAreaChartProps> = ({
             scaleTime({
                 range: [margin.left, innerWidth + margin.left],
                 domain: extent(
-                    data["1xx"],
+                    globalData,
                     getDate
                 ) as [Date, Date],
             }),
         [margin.left, width, height, data]
     );
+
     const valueScale = useMemo(
         () =>
             scaleLinear({
@@ -86,8 +104,8 @@ const StackedAreaChart: React.FunctionComponent<StackedAreaChartProps> = ({
                     0,
                     1.25 *
                     max(
-                        data["1xx"],
-                        getValue
+                        globalData,
+                        getMaxValue
                     ),
                 ],
                 nice: true,
@@ -141,37 +159,37 @@ const StackedAreaChart: React.FunctionComponent<StackedAreaChartProps> = ({
                     <AreaStack curve={visxCurve}>
                         <AreaSeries
                             dataKey="1xx"
-                            data={data["1xx"]}
+                            data={data}
                             xAccessor={getDate}
-                            yAccessor={getValue}
+                            yAccessor={get1xxValue}
                             fillOpacity={0.4}
                         />
                         <AreaSeries
                             dataKey="2xx"
-                            data={data["2xx"]}
+                            data={data}
                             xAccessor={getDate}
-                            yAccessor={getValue}
+                            yAccessor={get2xxValue}
                             fillOpacity={0.4}
                         />
                         <AreaSeries
                             dataKey="3xx"
-                            data={data["3xx"]}
+                            data={data}
                             xAccessor={getDate}
-                            yAccessor={getValue}
+                            yAccessor={get3xxValue}
                             fillOpacity={0.4}
                         />
                         <AreaSeries
                             dataKey="4xx"
-                            data={data["4xx"]}
+                            data={data}
                             xAccessor={getDate}
-                            yAccessor={getValue}
+                            yAccessor={get4xxValue}
                             fillOpacity={0.4}
                         />
                         <AreaSeries
                             dataKey="5xx"
-                            data={data["5xx"]}
+                            data={data}
                             xAccessor={getDate}
-                            yAccessor={getValue}
+                            yAccessor={get5xxValue}
                             fillOpacity={0.4}
                         />
                     </AreaStack>

+ 16 - 5
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts

@@ -6,10 +6,12 @@ import {
   MetricsMemoryDataResponse,
   MetricsNetworkDataResponse,
   MetricsNGINXErrorsDataResponse,
+  MetricsNGINXStatusDataResponse,
   AvailableMetrics,
   MetricsHpaReplicasDataResponse,
   MetricsNGINXLatencyDataResponse,
   NormalizedMetricsData,
+  NormalizedNginxStatusMetricsData,
 } from "./types";
 
 /**
@@ -42,9 +44,6 @@ export class MetricNormalizer {
     if (this.kind.includes("nginx:errors")) {
       return this.parseNGINXErrorsMetrics(this.metric_results);
     }
-    if (this.kind.includes("nginx:status")) {
-      return this.parseNGINXStatusMetrics(this.metric_results);
-    }
     if (
       this.kind.includes("nginx:latency") ||
       this.kind.includes("nginx:latency-histogram")
@@ -57,6 +56,14 @@ export class MetricNormalizer {
     return [];
   }
 
+  getNginxStatusData(): NormalizedNginxStatusMetricsData[] {
+    if (this.kind.includes("nginx:status")) {
+      return this.parseNGINXStatusMetrics(this.metric_results);
+    }
+
+    return []
+  }
+
   getAggregatedData(): Record<string, NormalizedMetricsData[]> {
     const groupedByDate = _.groupBy(this.getParsedData(), "date");
 
@@ -130,12 +137,16 @@ export class MetricNormalizer {
   }
 
   private parseNGINXStatusMetrics(
-    arr: MetricsNGINXErrorsDataResponse["results"]
+    arr: MetricsNGINXStatusDataResponse["results"]
   ) {
     return arr.map((d) => {
       return {
         date: d.date,
-        value: parseFloat(d.status_code),
+        "1xx": parseInt(d["1xx"]),
+        "2xx": parseInt(d["2xx"]),
+        "3xx": parseInt(d["3xx"]),
+        "4xx": parseInt(d["4xx"]),
+        "5xx": parseInt(d["5xx"]),
       };
     });
   }

+ 26 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/types.ts

@@ -38,6 +38,18 @@ export type MetricsNGINXLatencyDataResponse = {
   }[];
 };
 
+export type MetricsNGINXStatusDataResponse = {
+  pod?: string;
+  results: {
+    "1xx": string;
+    "2xx": string;
+    "3xx": string;
+    "4xx": string;
+    "5xx": string;
+    date: number;
+  }[];
+}
+
 export type MetricsHpaReplicasDataResponse = {
   pod?: string;
   results: {
@@ -49,13 +61,17 @@ export type MetricsHpaReplicasDataResponse = {
 export type GenericMetricResponse = {
   pod?: string;
   results: {
+    "1xx": string;
+    "2xx": string;
+    "3xx": string;
+    "4xx": string;
+    "5xx": string;
     date: number;
     cpu: string;
     memory: string;
     bytes: string;
     error_pct: string;
     replicas: string;
-    status_code: string;
     latency: string;
   }[];
 };
@@ -65,6 +81,15 @@ export type NormalizedMetricsData = {
   value: number; // value
 };
 
+export type NormalizedNginxStatusMetricsData = {
+  date: number; // unix timestamp
+  "1xx": number;
+  "2xx": number;
+  "3xx": number;
+  "4xx": number;
+  "5xx": number;
+};
+
 export type AvailableMetrics =
   | "cpu"
   | "memory"

+ 0 - 1
dashboard/src/shared/api.tsx

@@ -1316,7 +1316,6 @@ const getMetrics = baseApi<
     name?: string;
     percentile?: number;
     namespace: string;
-    nginx_status_level?: number;
     startrange: number;
     endrange: number;
     resolution: string;

+ 76 - 26
internal/kubernetes/prometheus/metrics.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"sort"
 	"strings"
 
 	v1 "k8s.io/api/core/v1"
@@ -116,8 +117,6 @@ type QueryOpts struct {
 	PodList   []string `schema:"pods"`
 	Name      string   `schema:"name"`
 	Namespace string   `schema:"namespace"`
-	// a prefix [1,2,3,4,5] used to scope nginx requests by when querying for status code responses
-	NginxStatusLevel uint `schema:"nginx_status_level"`
 	// start time (in unix timestamp) for prometheus results
 	StartRange uint `schema:"startrange"`
 	// end time time (in unix timestamp) for prometheus results
@@ -240,19 +239,7 @@ func QueryPrometheus(
 }
 
 func getNginxStatusQuery(opts *QueryOpts, selectionRegex string) (string, error) {
-	supportedLevels := map[int]bool{
-		1: true,
-		2: true,
-		3: true,
-		4: true,
-		5: true,
-	}
-
-	if !supportedLevels[int(opts.NginxStatusLevel)] {
-		return "", errors.New("invalid nginx status level specified")
-	}
-
-	query := fmt.Sprintf(`round(sum by (ingress)(increase(nginx_ingress_controller_requests{exported_namespace=~"%s",ingress="%s",service="%s",status=~"%d.."}[5m])), 0.001)`, opts.Namespace, selectionRegex, opts.Name, opts.NginxStatusLevel)
+	query := fmt.Sprintf(`round(sum by (status_code, ingress)(label_replace(increase(nginx_ingress_controller_requests{exported_namespace=~"%s",ingress="%s",service="%s"}[2m]), "status_code", "${1}xx", "status", "(.)..")), 0.001)`, opts.Namespace, selectionRegex, opts.Name)
 	return query, nil
 }
 
@@ -260,7 +247,8 @@ type promRawQuery struct {
 	Data struct {
 		Result []struct {
 			Metric struct {
-				Pod string `json:"pod,omitempty"`
+				Pod        string `json:"pod,omitempty"`
+				StatusCode string `json:"status_code,omitempty"`
 			} `json:"metric,omitempty"`
 
 			Values [][]interface{} `json:"values"`
@@ -269,14 +257,18 @@ type promRawQuery struct {
 }
 
 type promParsedSingletonQueryResult struct {
-	Date       interface{} `json:"date,omitempty"`
-	CPU        interface{} `json:"cpu,omitempty"`
-	Replicas   interface{} `json:"replicas,omitempty"`
-	Memory     interface{} `json:"memory,omitempty"`
-	Bytes      interface{} `json:"bytes,omitempty"`
-	ErrorPct   interface{} `json:"error_pct,omitempty"`
-	Latency    interface{} `json:"latency,omitempty"`
-	StatusCode interface{} `json:"status_code,omitempty"`
+	Date          interface{} `json:"date,omitempty"`
+	CPU           interface{} `json:"cpu,omitempty"`
+	Replicas      interface{} `json:"replicas,omitempty"`
+	Memory        interface{} `json:"memory,omitempty"`
+	Bytes         interface{} `json:"bytes,omitempty"`
+	ErrorPct      interface{} `json:"error_pct,omitempty"`
+	Latency       interface{} `json:"latency,omitempty"`
+	StatusCode1xx interface{} `json:"1xx,omitempty"`
+	StatusCode2xx interface{} `json:"2xx,omitempty"`
+	StatusCode3xx interface{} `json:"3xx,omitempty"`
+	StatusCode4xx interface{} `json:"4xx,omitempty"`
+	StatusCode5xx interface{} `json:"5xx,omitempty"`
 }
 
 type promParsedSingletonQuery struct {
@@ -285,6 +277,10 @@ type promParsedSingletonQuery struct {
 }
 
 func parseQuery(rawQuery []byte, metric string) ([]*promParsedSingletonQuery, error) {
+	if metric == "nginx:status" {
+		return parseNginxStatusQuery(rawQuery, metric)
+	}
+
 	rawQueryObj := &promRawQuery{}
 
 	err := json.Unmarshal(rawQuery, rawQueryObj)
@@ -314,8 +310,6 @@ func parseQuery(rawQuery []byte, metric string) ([]*promParsedSingletonQuery, er
 				singletonResult.Bytes = values[1]
 			} else if metric == "nginx:errors" {
 				singletonResult.ErrorPct = values[1]
-			} else if metric == "nginx:status" {
-				singletonResult.StatusCode = values[1]
 			} else if metric == "cpu_hpa_threshold" {
 				singletonResult.CPU = values[1]
 			} else if metric == "memory_hpa_threshold" {
@@ -337,6 +331,62 @@ func parseQuery(rawQuery []byte, metric string) ([]*promParsedSingletonQuery, er
 	return res, nil
 }
 
+func parseNginxStatusQuery(rawQuery []byte, metric string) ([]*promParsedSingletonQuery, error) {
+	rawQueryObj := &promRawQuery{}
+
+	err := json.Unmarshal(rawQuery, rawQueryObj)
+	if err != nil {
+		return nil, err
+	}
+
+	singletonResultsByDate := make(map[string]*promParsedSingletonQueryResult, 0)
+	keys := make([]string, 0)
+	for _, result := range rawQueryObj.Data.Result {
+		for _, values := range result.Values {
+			date := values[0]
+			dateKey := fmt.Sprintf("%v", date)
+
+			if _, ok := singletonResultsByDate[dateKey]; !ok {
+				keys = append(keys, dateKey)
+				singletonResultsByDate[dateKey] = &promParsedSingletonQueryResult{
+					Date: date,
+				}
+			}
+
+			switch result.Metric.StatusCode {
+			case "1xx":
+				singletonResultsByDate[dateKey].StatusCode1xx = values[1]
+			case "2xx":
+				singletonResultsByDate[dateKey].StatusCode2xx = values[1]
+			case "3xx":
+				singletonResultsByDate[dateKey].StatusCode3xx = values[1]
+			case "4xx":
+				singletonResultsByDate[dateKey].StatusCode4xx = values[1]
+			case "5xx":
+				singletonResultsByDate[dateKey].StatusCode5xx = values[1]
+			default:
+				return nil, errors.New("invalid nginx status code")
+			}
+		}
+	}
+
+	sort.Strings(keys)
+
+	singletonResults := make([]promParsedSingletonQueryResult, 0)
+	for _, k := range keys {
+		singletonResults = append(singletonResults, *singletonResultsByDate[k])
+	}
+
+	singleton := &promParsedSingletonQuery{
+		Results: singletonResults,
+	}
+
+	res := make([]*promParsedSingletonQuery, 0)
+	res = append(res, singleton)
+
+	return res, nil
+}
+
 func getSelectionRegex(kind, name string) (string, error) {
 	var suffix string
 

+ 3 - 29
internal/kubernetes/prometheus/metrics_test.go

@@ -1,7 +1,6 @@
 package prometheus
 
 import (
-	"errors"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -25,42 +24,17 @@ func Test_getNginxStatusQuery(t *testing.T) {
 			err   error
 		}
 	}{
-		{
-			"missing status level",
-			input{
-				&QueryOpts{},
-				"process-app-web",
-			},
-			output{
-				"",
-				errors.New("invalid nginx status level specified"),
-			},
-		},
-		{
-			"invalid status level",
-			input{
-				&QueryOpts{
-					NginxStatusLevel: 18,
-				},
-				"process-app-web",
-			},
-			output{
-				"",
-				errors.New("invalid nginx status level specified"),
-			},
-		},
 		{
 			"valid status level",
 			input{
 				&QueryOpts{
-					Name:             "process-app-web",
-					Namespace:        "app-namespace",
-					NginxStatusLevel: 2,
+					Name:      "process-app-web",
+					Namespace: "app-namespace",
 				},
 				"process-app-web",
 			},
 			output{
-				`round(sum by (ingress)(irate(nginx_ingress_controller_requests{exported_namespace=~"app-namespace",ingress="process-app-web",service="process-app-web",status=~"2.."}[5m])), 0.001)`,
+				`round(sum by (status_code, ingress)(label_replace(increase(nginx_ingress_controller_requests{exported_namespace=~"app-namespace",ingress="process-app-web",service="process-app-web"}[2m]), "status_code", "${1}xx", "status", "(.)..")) by (status_code, ingress), 0.001)`,
 				nil,
 			},
 		},