Path: blob/main/components/ws-proxy/pkg/proxy/metrics.go
2500 views
// Copyright (c) 2024 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package proxy56import (7"context"8"net/http"9"strings"1011"github.com/gitpod-io/gitpod/common-go/log"12"github.com/gorilla/mux"13"github.com/prometheus/client_golang/prometheus"14"github.com/prometheus/client_golang/prometheus/promhttp"15"sigs.k8s.io/controller-runtime/pkg/metrics"16)1718const (19metricsNamespace = "gitpod"20metricsSubsystem = "ws_proxy"21)2223type httpMetrics struct {24requestsTotal *prometheus.CounterVec25requestsDuration *prometheus.HistogramVec26}2728func (m *httpMetrics) Describe(ch chan<- *prometheus.Desc) {29m.requestsTotal.Describe(ch)30m.requestsDuration.Describe(ch)31}3233func (m *httpMetrics) Collect(ch chan<- prometheus.Metric) {34m.requestsTotal.Collect(ch)35m.requestsDuration.Collect(ch)36}3738var (39serverMetrics = &httpMetrics{40requestsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{41Namespace: metricsNamespace,42Subsystem: metricsSubsystem,43Name: "http_server_requests_total",44Help: "Total number of incoming HTTP requests",45}, []string{"method", "resource", "code", "http_version"}),46requestsDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{47Namespace: metricsNamespace,48Subsystem: metricsSubsystem,49Name: "http_server_requests_duration_seconds",50Help: "Duration of incoming HTTP requests in seconds",51Buckets: []float64{.005, .025, .05, .1, .5, 1, 2.5, 5, 30, 60, 120, 240, 600},52}, []string{"method", "resource", "code", "http_version"}),53}54clientMetrics = &httpMetrics{55requestsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{56Namespace: metricsNamespace,57Subsystem: metricsSubsystem,58Name: "http_client_requests_total",59Help: "Total number of outgoing HTTP requests",60}, []string{"method", "resource", "code", "http_version"}),61requestsDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{62Namespace: metricsNamespace,63Subsystem: metricsSubsystem,64Name: "http_client_requests_duration_seconds",65Help: "Duration of outgoing HTTP requests in seconds",66Buckets: []float64{.005, .025, .05, .1, .5, 1, 2.5, 5, 30, 60, 120, 240, 600},67}, []string{"method", "resource", "code", "http_version"}),68}69)7071func init() {72metrics.Registry.MustRegister(serverMetrics, clientMetrics)73}7475type contextKey int7677var (78resourceKey = contextKey(0)79httpVersionKey = contextKey(1)80)8182func withResourceMetricsLabel(r *http.Request, resource string) *http.Request {83ctx := context.WithValue(r.Context(), resourceKey, []string{resource})84return r.WithContext(ctx)85}8687func withResourceLabel() promhttp.Option {88return promhttp.WithLabelFromCtx("resource", func(ctx context.Context) string {89if v := ctx.Value(resourceKey); v != nil {90if resources, ok := v.([]string); ok {91if len(resources) > 0 {92return resources[0]93}94}95}96return "unknown"97})98}99100func withHttpVersionMetricsLabel(r *http.Request) *http.Request {101ctx := context.WithValue(r.Context(), httpVersionKey, []string{r.Proto})102return r.WithContext(ctx)103}104105func withHttpVersionLabel() promhttp.Option {106return promhttp.WithLabelFromCtx("http_version", func(ctx context.Context) string {107if v := ctx.Value(httpVersionKey); v != nil {108if versions, ok := v.([]string); ok {109if len(versions) > 0 {110return versions[0]111}112}113}114return "unknown"115})116}117118func instrumentClientMetrics(transport http.RoundTripper) http.RoundTripper {119return promhttp.InstrumentRoundTripperCounter(clientMetrics.requestsTotal,120promhttp.InstrumentRoundTripperDuration(clientMetrics.requestsDuration,121transport,122withResourceLabel(),123withHttpVersionLabel(),124),125withResourceLabel(),126withHttpVersionLabel(),127)128}129130func instrumentServerMetrics(next http.Handler) http.Handler {131handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {132next.ServeHTTP(w, req)133if v := req.Context().Value(resourceKey); v != nil {134if resources, ok := v.([]string); ok {135if len(resources) > 0 {136resources[0] = getHandlerResource(req)137}138}139}140if v := req.Context().Value(httpVersionKey); v != nil {141if versions, ok := v.([]string); ok {142if len(versions) > 0 {143versions[0] = req.Proto144}145}146}147})148instrumented := promhttp.InstrumentHandlerCounter(serverMetrics.requestsTotal,149promhttp.InstrumentHandlerDuration(serverMetrics.requestsDuration,150handler,151withResourceLabel(),152withHttpVersionLabel(),153),154withResourceLabel(),155withHttpVersionLabel(),156)157return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {158ctx := context.WithValue(req.Context(), resourceKey, []string{"unknown"})159ctx = context.WithValue(ctx, httpVersionKey, []string{"unknown"})160instrumented.ServeHTTP(w, req.WithContext(ctx))161})162}163164func getHandlerResource(req *http.Request) string {165hostPart := getResourceHost(req)166if hostPart == "" {167hostPart = "unknown"168log.WithField("URL", req.URL).Warn("client metrics: cannot determine resource host part")169}170171routePart := ""172if route := mux.CurrentRoute(req); route != nil {173routePart = route.GetName()174}175if routePart == "" {176log.WithField("URL", req.URL).Warn("client metrics: cannot determine resource route part")177routePart = "unknown"178}179if routePart == "root" {180routePart = ""181} else {182routePart = "/" + routePart183}184return hostPart + routePart185}186187func getResourceHost(req *http.Request) string {188coords := getWorkspaceCoords(req)189190var parts []string191192if coords.Foreign {193parts = append(parts, "foreign_content")194}195196if coords.ID != "" {197workspacePart := "workspace"198if coords.Debug {199workspacePart = "debug_" + workspacePart200}201if coords.Port != "" {202workspacePart += "_port"203}204parts = append(parts, workspacePart)205}206return strings.Join(parts, "/")207}208209210