From e80702e60d1cce1edb2f0d1185d0e980d6692fe9 Mon Sep 17 00:00:00 2001 From: Allen Bai Date: Wed, 29 Sep 2021 09:10:35 -0400 Subject: [PATCH] metrics: add /metrics endpoint and console_helm_install_count metric Works with https://github.com/openshift/console-operator/pull/601 to add a console_helm_install_count counter vector metric. This change will increment the counter each time a user installs a Helm chart in the console. Closes: https://issues.redhat.com/browse/HELM-235 Signed-off-by: Allen Bai --- pkg/helm/actions/install_chart.go | 4 + pkg/helm/metrics/metrics.go | 34 ++++++++ pkg/helm/metrics/metrics_test.go | 136 ++++++++++++++++++++++++++++++ pkg/server/middleware.go | 7 ++ pkg/server/server.go | 5 ++ 5 files changed, 186 insertions(+) create mode 100644 pkg/helm/metrics/metrics.go create mode 100644 pkg/helm/metrics/metrics_test.go diff --git a/pkg/helm/actions/install_chart.go b/pkg/helm/actions/install_chart.go index ee8bca36edaa..93e2ee58f171 100644 --- a/pkg/helm/actions/install_chart.go +++ b/pkg/helm/actions/install_chart.go @@ -1,6 +1,7 @@ package actions import ( + "github.com/openshift/console/pkg/helm/metrics" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" @@ -40,5 +41,8 @@ func InstallChart(ns, name, url string, vals map[string]interface{}, conf *actio if err != nil { return nil, err } + + metrics.HandleConsoleHelmInstallCount(ch.Metadata.Name, ch.Metadata.Version) + return release, nil } diff --git a/pkg/helm/metrics/metrics.go b/pkg/helm/metrics/metrics.go new file mode 100644 index 000000000000..941f09b8f598 --- /dev/null +++ b/pkg/helm/metrics/metrics.go @@ -0,0 +1,34 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "k8s.io/klog/v2" +) + +var ( + consoleHelmInstallCount = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "console_helm_install_count", + Help: "Number of Helm installation from console.", + }, + []string{"console_helm_chart_name", "console_helm_chart_version"}, + ) +) + +func init() { + prometheus.MustRegister(consoleHelmInstallCount) +} + +func HandleConsoleHelmInstallCount(chartName, chartVersion string) { + defer recoverMetricPanic() + + klog.V(4).Infof("metric console_helm_install_count: %s %s", chartName, chartVersion) + consoleHelmInstallCount.WithLabelValues(chartName, chartVersion).Add(1) +} + +// Reference: https://github.com/openshift/console-operator/blob/master/pkg/console/metrics/metrics.go#L80 +func recoverMetricPanic() { + if r := recover(); r != nil { + klog.Errorf("Recovering from metric function - %v", r) + } +} diff --git a/pkg/helm/metrics/metrics_test.go b/pkg/helm/metrics/metrics_test.go new file mode 100644 index 000000000000..9da6182e7507 --- /dev/null +++ b/pkg/helm/metrics/metrics_test.go @@ -0,0 +1,136 @@ +package metrics + +import ( + "bufio" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const ( + consoleHelmInstallCountMetric = "console_helm_install_count" +) + +func TestHandleConsoleHelmInstallCountNoRelease(t *testing.T) { + consoleHelmInstallCount.Reset() + ts := httptest.NewServer(promhttp.Handler()) + defer ts.Close() + + count := countMetric(t, ts, consoleHelmInstallCountMetric) + if count > 0 { + t.Errorf("%s should not be available", consoleHelmInstallCountMetric) + } +} + +func TestHandleConsoleHelmInstallCountSingleRelease(t *testing.T) { + consoleHelmInstallCount.Reset() + ts := httptest.NewServer(promhttp.Handler()) + defer ts.Close() + + chartName, chartVersion := "test-chart", "0.0.1" + chartNameLabel, chartVersionLabel := fmt.Sprintf("console_helm_chart_name=\"%v\"", chartName), fmt.Sprintf("console_helm_chart_version=\"%v\"", chartVersion) + HandleConsoleHelmInstallCount(chartName, chartVersion) + + count := countMetric(t, ts, consoleHelmInstallCountMetric, chartNameLabel, chartVersionLabel) + if count != 1 { + t.Errorf("%s with labels %s, %s should be 1: %v", consoleHelmInstallCountMetric, chartNameLabel, chartVersionLabel, count) + } +} + +func TestHandleConsoleHelmInstallCountMultipleReleases(t *testing.T) { + consoleHelmInstallCount.Reset() + ts := httptest.NewServer(promhttp.Handler()) + defer ts.Close() + + chartName, chartVersion := "test-chart", "0.0.1" + chartNameLabel, chartVersionLabel := fmt.Sprintf("console_helm_chart_name=\"%v\"", chartName), fmt.Sprintf("console_helm_chart_version=\"%v\"", chartVersion) + HandleConsoleHelmInstallCount(chartName, chartVersion) + HandleConsoleHelmInstallCount(chartName, chartVersion) + + count := countMetric(t, ts, consoleHelmInstallCountMetric, chartNameLabel, chartVersionLabel) + if count != 2 { + t.Errorf("%s with labels %s, %s should be 2: %v", consoleHelmInstallCountMetric, chartNameLabel, chartVersionLabel, count) + } + + chartName, chartVersion = "test-chart-2", "0.0.2" + chartNameLabel, chartVersionLabel = fmt.Sprintf("console_helm_chart_name=\"%v\"", chartName), fmt.Sprintf("console_helm_chart_version=\"%v\"", chartVersion) + HandleConsoleHelmInstallCount(chartName, chartVersion) + + count = countMetric(t, ts, consoleHelmInstallCountMetric, chartNameLabel, chartVersionLabel) + if count != 1 { + t.Errorf("%s with labels %s, %s should be 1: %v", consoleHelmInstallCountMetric, chartNameLabel, chartVersionLabel, count) + } + + count = countMetric(t, ts, consoleHelmInstallCountMetric) + if count != 3 { + t.Errorf("%s without labels should be 3: %v", consoleHelmInstallCountMetric, count) + } + +} + +func getMetrics(t *testing.T, ts *httptest.Server) *http.Response { + res, err := http.Get(ts.URL + "/metrics") + if err != nil { + t.Errorf("http error: %s", err) + } + + if !httpOK(res) { + t.Errorf("http error: %d %s", res.StatusCode, http.StatusText(res.StatusCode)) + } + return res +} + +func countMetric(t *testing.T, ts *httptest.Server, metric string, labels ...string) int { + res := getMetrics(t, ts) + defer res.Body.Close() + + bytes, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("read error: %s", err) + } + + return countMetricWithLabels(t, string(bytes), metric, labels...) +} + +func countMetricWithLabels(t *testing.T, response, metric string, labels ...string) (count int) { + scanner := bufio.NewScanner(strings.NewReader(response)) + for scanner.Scan() { + text := scanner.Text() + // skip comments + if strings.HasPrefix(text, "#") { + continue + } + if strings.Contains(text, metric) { + t.Logf("found %s\n", scanner.Text()) + curr_count, _ := strconv.Atoi(text[len(text)-1:]) + // no specific labels, count all + if len(labels) == 0 { + count += curr_count + } + // return metric value with specified labels + for i, label := range labels { + if !strings.Contains(text, label) { + break + } + if i == len(labels)-1 { + // return directly since metrics are aggregated + return curr_count + } + } + } + } + return count +} + +func httpOK(resp *http.Response) bool { + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + return true + } + return false +} diff --git a/pkg/server/middleware.go b/pkg/server/middleware.go index 62b773ebb9bf..1e05f7f695a8 100644 --- a/pkg/server/middleware.go +++ b/pkg/server/middleware.go @@ -23,6 +23,13 @@ func authMiddleware(a *auth.Authenticator, hdlr http.HandlerFunc) http.Handler { func authMiddlewareWithUser(a *auth.Authenticator, handlerFunc func(user *auth.User, w http.ResponseWriter, r *http.Request)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Requests from prometheus-k8s have the access token in headers instead of cookies. + // This allows metric requests with proper tokens in either headers or cookies. + if r.URL.Path == "/metrics" { + openshiftSessionCookieName := "openshift-session-token" + openshiftSessionCookieValue := r.Header.Get("Authorization") + r.AddCookie(&http.Cookie{Name: openshiftSessionCookieName, Value: openshiftSessionCookieValue}) + } user, err := a.Authenticate(r) if err != nil { klog.V(4).Infof("authentication failed: %v", err) diff --git a/pkg/server/server.go b/pkg/server/server.go index 7bae7aba4755..8796c114c4cc 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -17,6 +17,7 @@ import ( "time" "github.com/coreos/pkg/health" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/openshift/console/pkg/auth" "github.com/openshift/console/pkg/graphql/resolver" @@ -470,6 +471,10 @@ func (s *Server) HTTPHandler() http.Handler { }) // Helm Endpoints + handle("/metrics", authHandler(func(w http.ResponseWriter, r *http.Request) { + promhttp.Handler().ServeHTTP(w, r) + })) + handle("/api/helm/template", authHandlerWithUser(helmHandlers.HandleHelmRenderManifests)) handle("/api/helm/releases", authHandlerWithUser(helmHandlers.HandleHelmList)) handle("/api/helm/chart", authHandlerWithUser(helmHandlers.HandleChartGet))