Skip to content

Commit

Permalink
metrics: add /metrics endpoint and console_helm_install_count metric
Browse files Browse the repository at this point in the history
Works with openshift/console-operator#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 <abai@redhat.com>
  • Loading branch information
Allen Bai committed Oct 7, 2021
1 parent 92c113f commit e80702e
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 0 deletions.
4 changes: 4 additions & 0 deletions 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"
Expand Down Expand Up @@ -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
}
34 changes: 34 additions & 0 deletions 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)
}
}
136 changes: 136 additions & 0 deletions 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
}
7 changes: 7 additions & 0 deletions pkg/server/middleware.go
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions pkg/server/server.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit e80702e

Please sign in to comment.