Просмотр исходного кода

nginx custom metrics integration'

Alexander Belanger 5 лет назад
Родитель
Сommit
89e36279a4

+ 109 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx

@@ -17,7 +17,9 @@ type PropsType = {
 
 type StateType = {
   controllerOptions: any[];
+  ingressOptions: any[];
   selectedController: any;
+  selectedIngress: any;
   pods: any[];
   selectedPod: string;
   selectedRange: string;
@@ -28,6 +30,7 @@ type StateType = {
   dropdownExpanded: boolean;
   data: MetricsData[];
   showMetricsSettings: boolean;
+  metricsOptions: MetricsOption[];
 };
 
 type MetricsCPUDataResponse = {
@@ -54,6 +57,19 @@ type MetricsNetworkDataResponse = {
   }[];
 }[];
 
+type MetricsNGINXErrorsDataResponse = {
+  pod?: string;
+  results: {
+    date: number;
+    error_pct: string;
+  }[];
+}[];
+
+type MetricsOption = {
+  value: string;
+  label: string;
+}
+
 const resolutions: { [range: string]: string } = {
   "1H": "15s",
   "6H": "15s",
@@ -74,6 +90,8 @@ export default class MetricsSection extends Component<PropsType, StateType> {
     selectedPod: "",
     controllerOptions: [] as any[],
     selectedController: null as any,
+    ingressOptions: [] as any[],
+    selectedIngress: null as any,
     selectedRange: "1H",
     selectedMetric: "cpu",
     selectedMetricLabel: "CPU Utilization (vCPUs)",
@@ -82,12 +100,51 @@ export default class MetricsSection extends Component<PropsType, StateType> {
     controllerDropdownExpanded: false,
     data: [] as MetricsData[],
     showMetricsSettings: false,
+    metricsOptions: [
+      { value: "cpu", label: "CPU Utilization (vCPUs)" },
+      { value: "memory", label: "RAM Utilization (Mi)" },
+      { value: "network", label: "Network Received Bytes (Ki)" },
+    ],
   };
 
   componentDidMount() {
     // get all controllers and read in a list of pods
     let { currentChart } = this.props;
     let { currentCluster, currentProject, setCurrentError } = this.context;
+    
+    if (currentChart.chart?.metadata?.name == "ingress-nginx") {
+      api.getNGINXIngresses(
+        "<token>",
+          {
+            cluster_id: currentCluster.id,
+          },
+          {
+            id: currentProject.id,
+          }
+      ).then((res) => {
+        let metricsOptions = this.state.metricsOptions
+        metricsOptions.push({
+          value: "nginx:errors",
+          label: "5XX Error Percentage"
+        })
+
+        let ingressOptions = [] as any[];
+        res.data.map((ingress: string) => {
+          ingressOptions.push({ value: ingress, label: ingress });
+        });
+
+        // iterate through the controllers to get the list of pods
+        this.setState({
+          metricsOptions,
+          ingressOptions,
+          selectedIngress: ingressOptions[0].value,
+        });
+      })
+      .catch((err) => {
+        setCurrentError(JSON.stringify(err));
+        this.setState({ controllerOptions: [] as any[] });
+      });
+    }
 
     api
       .getChartControllers(
@@ -145,6 +202,10 @@ export default class MetricsSection extends Component<PropsType, StateType> {
     ) {
       this.getMetrics();
     }
+
+    if (this.state.selectedIngress != prevState.selectedIngress) {
+      this.getMetrics();
+    }
   }
 
   getMetrics = () => {
@@ -170,6 +231,11 @@ export default class MetricsSection extends Component<PropsType, StateType> {
       pods = [this.state.selectedPod];
     }
 
+    if (this.state.selectedMetric == "nginx:errors") {
+      pods = [this.state.selectedIngress]
+      shouldsum = false
+    }
+
     api
       .getMetrics(
         "<token>",
@@ -246,6 +312,25 @@ export default class MetricsSection extends Component<PropsType, StateType> {
             }
           );
 
+          this.setState({ data: tData });
+        } else if (kind == "nginx:errors") {
+          let data = res.data as MetricsNGINXErrorsDataResponse;
+
+          let tData = data[0].results.map(
+            (
+              d: {
+                date: number;
+                error_pct: string;
+              },
+              i: number
+            ) => {
+              return {
+                date: d.date,
+                value: parseFloat(d.error_pct), // put units in Ki
+              };
+            }
+          );
+
           this.setState({ data: tData });
         }
       })
@@ -323,12 +408,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
   };
 
   renderOptionList = () => {
-    let metricOptions = [
-      { value: "cpu", label: "CPU Utilization (vCPUs)" },
-      { value: "memory", label: "RAM Utilization (Mi)" },
-      { value: "network", label: "Network Received Bytes (Ki)" },
-    ];
-    return metricOptions.map(
+    return this.state.metricsOptions.map(
       (option: { value: string; label: string }, i: number) => {
         return (
           <Option
@@ -340,7 +420,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
                 selectedMetricLabel: option.label,
               })
             }
-            lastItem={i === metricOptions.length - 1}
+            lastItem={i === this.state.metricsOptions.length - 1}
           >
             {option.label}
           </Option>
@@ -351,6 +431,28 @@ export default class MetricsSection extends Component<PropsType, StateType> {
 
   renderMetricsSettings = () => {
     if (this.state.showMetricsSettings && true) {
+      if (this.state.selectedMetric == "nginx:errors") {
+        return (
+          <>
+          <DropdownOverlay
+            onClick={() => this.setState({ showMetricsSettings: false })}
+          />
+          <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
+            <Label>Additional Settings</Label>
+            <SelectRow
+              label="Target Ingress"
+              value={this.state.selectedIngress}
+              setActiveValue={(x: any) =>
+                this.setState({ selectedIngress: x })
+              }
+              options={this.state.ingressOptions}
+              width="100%"
+            />
+          </DropdownAlt>
+        </>
+        )
+      }
+
       return (
         <>
           <DropdownOverlay

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

@@ -451,6 +451,15 @@ const getNamespaces = baseApi<
   return `/api/projects/${pathParams.id}/k8s/namespaces`;
 });
 
+const getNGINXIngresses = baseApi<
+  {
+    cluster_id: number;
+  },
+  { id: number }
+>("GET", pathParams => {
+  return `/api/projects/${pathParams.id}/k8s/prometheus/ingresses`;
+});
+
 const getOAuthIds = baseApi<
   {},
   {
@@ -716,6 +725,7 @@ export default {
   getMatchingPods,
   getMetrics,
   getNamespaces,
+  getNGINXIngresses,
   getOAuthIds,
   getProjectClusters,
   getProjectRegistries,

+ 33 - 4
internal/kubernetes/prometheus/metrics.go

@@ -29,6 +29,28 @@ func GetPrometheusService(clientset kubernetes.Interface) (*v1.Service, bool, er
 	return &services.Items[0], true, nil
 }
 
+// GetIngressesWithNGINXAnnotation gets an array of names for all ingresses controlled by
+// NGINX
+func GetIngressesWithNGINXAnnotation(clientset kubernetes.Interface) ([]string, error) {
+	ingressList, err := clientset.NetworkingV1beta1().Ingresses("").List(context.TODO(), metav1.ListOptions{})
+
+	if err != nil {
+		return nil, err
+	}
+
+	res := make([]string, 0)
+
+	for _, ingress := range ingressList.Items {
+		if ingressAnn, found := ingress.ObjectMeta.Annotations["kubernetes.io/ingress.class"]; found {
+			if ingressAnn == "nginx" {
+				res = append(res, ingress.ObjectMeta.Name)
+			}
+		}
+	}
+
+	return res, nil
+}
+
 type QueryOpts struct {
 	Metric     string   `schema:"metric"`
 	ShouldSum  bool     `schema:"shouldsum"`
@@ -58,6 +80,10 @@ func QueryPrometheus(
 	} else if opts.Metric == "network" {
 		netPodSelector := fmt.Sprintf(`namespace="%s",pod=~"%s",container="POD"`, opts.Namespace, strings.Join(opts.PodList, "|"))
 		query = fmt.Sprintf("rate(container_network_receive_bytes_total{%s}[5m])", netPodSelector)
+	} else if opts.Metric == "nginx:errors" {
+		num := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%s",ingress=~"%s"}[5m]) OR on() vector(0))`, opts.Namespace, strings.Join(opts.PodList, "|"))
+		denom := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{namespace="%s",ingress=~"%s"}[5m]) > 0)`, opts.Namespace, strings.Join(opts.PodList, "|"))
+		query = fmt.Sprintf(`%s / %s * 100 OR on() vector(0)`, num, denom)
 	}
 
 	if opts.ShouldSum {
@@ -101,10 +127,11 @@ type promRawQuery struct {
 }
 
 type promParsedSingletonQueryResult struct {
-	Date   interface{} `json:"date,omitempty"`
-	CPU    interface{} `json:"cpu,omitempty"`
-	Memory interface{} `json:"memory,omitempty"`
-	Bytes  interface{} `json:"bytes,omitempty"`
+	Date     interface{} `json:"date,omitempty"`
+	CPU      interface{} `json:"cpu,omitempty"`
+	Memory   interface{} `json:"memory,omitempty"`
+	Bytes    interface{} `json:"bytes,omitempty"`
+	ErrorPct interface{} `json:"error_pct,omitempty"`
 }
 
 type promParsedSingletonQuery struct {
@@ -137,6 +164,8 @@ func parseQuery(rawQuery []byte, metric string) ([]byte, error) {
 				singletonResult.Memory = values[1]
 			} else if metric == "network" {
 				singletonResult.Bytes = values[1]
+			} else if metric == "nginx:errors" {
+				singletonResult.ErrorPct = values[1]
 			}
 
 			singletonResults = append(singletonResults, *singletonResult)

+ 49 - 0
server/api/k8s_handler.go

@@ -371,6 +371,55 @@ func (app *App) HandleDetectPrometheusInstalled(w http.ResponseWriter, r *http.R
 	return
 }
 
+// HandleListNGINXIngresses lists all NGINX ingresses in a target cluster
+func (app *App) HandleListNGINXIngresses(w http.ResponseWriter, r *http.Request) {
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	// get the filter options
+	form := &forms.K8sForm{
+		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
+		},
+	}
+
+	form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	// create a new agent
+	var agent *kubernetes.Agent
+
+	if app.ServerConf.IsTesting {
+		agent = app.TestAgents.K8sAgent
+	} else {
+		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
+	}
+
+	res, err := prometheus.GetIngressesWithNGINXAnnotation(agent.Clientset)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(res); err != nil {
+		app.handleErrorFormDecoding(err, ErrK8sDecode, w)
+		return
+	}
+}
+
 func (app *App) HandleGetPodMetrics(w http.ResponseWriter, r *http.Request) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 

+ 14 - 0
server/router/router.go

@@ -1098,6 +1098,20 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/k8s/prometheus/ingresses",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleListNGINXIngresses, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
 		r.Method(
 			"GET",
 			"/projects/{project_id}/k8s/metrics",