Skip to content

Commit

Permalink
feat: expose HTTP requests metrics
Browse files Browse the repository at this point in the history
Signed-off-by: Vivek Singh <[email protected]>
  • Loading branch information
viveksyngh authored and prometherion committed Jun 22, 2022
1 parent 7e71174 commit 8ed733e
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 1 deletion.
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,5 @@ linters:
- goerr113
- exhaustivestruct
- lll

service:
golangci-lint-version: 1.45.x
66 changes: 66 additions & 0 deletions internal/webserver/middleware/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package middleware

import (
"net/http"
"strconv"

"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

func init() {
_ = prometheus.Register(totalRequests)
_ = prometheus.Register(httpDuration)
}

type httpResponseWriter struct {
http.ResponseWriter
statusCode int
}

func newHTTPResponseWriter(w http.ResponseWriter) *httpResponseWriter {
return &httpResponseWriter{
w,
http.StatusOK,
}
}

func (h *httpResponseWriter) WriteHeader(statusCode int) {
h.statusCode = statusCode
h.ResponseWriter.WriteHeader(statusCode)
}

var totalRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "capsule_proxy_requests_total",
Help: "Number of requests",
},
[]string{"path", "status"},
)

var httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "capsule_proxy_response_time_seconds",
Help: "Duration of capsule proxy requests.",
}, []string{"path"})

func MetricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()

timer := prometheus.NewTimer(httpDuration.WithLabelValues(path))

rw := newHTTPResponseWriter(w)
next.ServeHTTP(rw, r)

statusCode := rw.statusCode

totalRequests.WithLabelValues(path, strconv.Itoa(statusCode)).Inc()

timer.ObserveDuration()
})
}
90 changes: 90 additions & 0 deletions internal/webserver/middleware/metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package middleware

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
model "github.com/prometheus/client_model/go"
)

func dummyHandler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("hello"))
w.WriteHeader(http.StatusOK)
}

func newRequest(method, url string) (*http.Request, error) {
req, err := http.NewRequest(method, url, nil)
return req, err
}

func Test_MetricsMiddleware_RequestCount(t *testing.T) {
testCases := []struct {
name string
requestCount int
path string
output float64
}{
{

name: "single request count",
requestCount: 1,
path: "/test",
output: 1,
},
}

for _, test := range testCases {
router := mux.NewRouter()
router.HandleFunc(test.path, dummyHandler).Methods("GET")
router.Use(MetricsMiddleware)

rw := httptest.NewRecorder()

for i := 0; i < test.requestCount; i++ {
req, err := newRequest("GET", test.path)
if err != nil {
t.Errorf("failed to create HTTP request object")
}
router.ServeHTTP(rw, req)
}

t.Run("regular middleware call", func(t *testing.T) {
ch := make(chan prometheus.Metric)
go totalRequests.Collect(ch)
g := (<-ch).(prometheus.Counter)
result := readVector(g)
if test.output != result.value {
t.Errorf("testcase %s failed. expected: %f, got: %f", test.name, test.output, result.value)
}
})
}
}

type metricResult struct {
value float64
labels map[string]string
}

func labels2Map(labels []*model.LabelPair) map[string]string {
res := map[string]string{}
for _, l := range labels {
res[l.GetName()] = l.GetValue()
}
return res
}

func readVector(g prometheus.Metric) metricResult {
m := &model.Metric{}
_ = g.Write(m)

return metricResult{
value: m.GetCounter().GetValue(),
labels: labels2Map(m.GetLabel()),
}
}
4 changes: 4 additions & 0 deletions internal/webserver/webserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/net/http/httpguts"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/sets"
Expand Down Expand Up @@ -259,14 +260,17 @@ func (n kubeFilter) registerModules(ctx context.Context, root *mux.Router) {
func (n kubeFilter) Start(ctx context.Context) error {
r := mux.NewRouter().StrictSlash(true)
r.Use(handlers.RecoveryHandler())

r.Path("/_healthz").Subrouter().HandleFunc("", func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(200)
_, _ = writer.Write([]byte("ok"))
})
r.Path("/_metrics").Subrouter().Handle("", promhttp.Handler())

root := r.PathPrefix("").Subrouter()
n.registerModules(ctx, root)
root.Use(
middleware.MetricsMiddleware,
n.reverseProxyMiddleware,
middleware.CheckPaths(n.client, n.log, n.allowedPaths, n.impersonateHandler),
middleware.CheckAuthorization(n.client, n.log, n.serverOptions.IsListeningTLS()),
Expand Down

0 comments on commit 8ed733e

Please sign in to comment.