Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-proxy/pkg/proxy/metrics.go
2500 views
1
// Copyright (c) 2024 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package proxy
6
7
import (
8
"context"
9
"net/http"
10
"strings"
11
12
"github.com/gitpod-io/gitpod/common-go/log"
13
"github.com/gorilla/mux"
14
"github.com/prometheus/client_golang/prometheus"
15
"github.com/prometheus/client_golang/prometheus/promhttp"
16
"sigs.k8s.io/controller-runtime/pkg/metrics"
17
)
18
19
const (
20
metricsNamespace = "gitpod"
21
metricsSubsystem = "ws_proxy"
22
)
23
24
type httpMetrics struct {
25
requestsTotal *prometheus.CounterVec
26
requestsDuration *prometheus.HistogramVec
27
}
28
29
func (m *httpMetrics) Describe(ch chan<- *prometheus.Desc) {
30
m.requestsTotal.Describe(ch)
31
m.requestsDuration.Describe(ch)
32
}
33
34
func (m *httpMetrics) Collect(ch chan<- prometheus.Metric) {
35
m.requestsTotal.Collect(ch)
36
m.requestsDuration.Collect(ch)
37
}
38
39
var (
40
serverMetrics = &httpMetrics{
41
requestsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
42
Namespace: metricsNamespace,
43
Subsystem: metricsSubsystem,
44
Name: "http_server_requests_total",
45
Help: "Total number of incoming HTTP requests",
46
}, []string{"method", "resource", "code", "http_version"}),
47
requestsDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
48
Namespace: metricsNamespace,
49
Subsystem: metricsSubsystem,
50
Name: "http_server_requests_duration_seconds",
51
Help: "Duration of incoming HTTP requests in seconds",
52
Buckets: []float64{.005, .025, .05, .1, .5, 1, 2.5, 5, 30, 60, 120, 240, 600},
53
}, []string{"method", "resource", "code", "http_version"}),
54
}
55
clientMetrics = &httpMetrics{
56
requestsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
57
Namespace: metricsNamespace,
58
Subsystem: metricsSubsystem,
59
Name: "http_client_requests_total",
60
Help: "Total number of outgoing HTTP requests",
61
}, []string{"method", "resource", "code", "http_version"}),
62
requestsDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
63
Namespace: metricsNamespace,
64
Subsystem: metricsSubsystem,
65
Name: "http_client_requests_duration_seconds",
66
Help: "Duration of outgoing HTTP requests in seconds",
67
Buckets: []float64{.005, .025, .05, .1, .5, 1, 2.5, 5, 30, 60, 120, 240, 600},
68
}, []string{"method", "resource", "code", "http_version"}),
69
}
70
)
71
72
func init() {
73
metrics.Registry.MustRegister(serverMetrics, clientMetrics)
74
}
75
76
type contextKey int
77
78
var (
79
resourceKey = contextKey(0)
80
httpVersionKey = contextKey(1)
81
)
82
83
func withResourceMetricsLabel(r *http.Request, resource string) *http.Request {
84
ctx := context.WithValue(r.Context(), resourceKey, []string{resource})
85
return r.WithContext(ctx)
86
}
87
88
func withResourceLabel() promhttp.Option {
89
return promhttp.WithLabelFromCtx("resource", func(ctx context.Context) string {
90
if v := ctx.Value(resourceKey); v != nil {
91
if resources, ok := v.([]string); ok {
92
if len(resources) > 0 {
93
return resources[0]
94
}
95
}
96
}
97
return "unknown"
98
})
99
}
100
101
func withHttpVersionMetricsLabel(r *http.Request) *http.Request {
102
ctx := context.WithValue(r.Context(), httpVersionKey, []string{r.Proto})
103
return r.WithContext(ctx)
104
}
105
106
func withHttpVersionLabel() promhttp.Option {
107
return promhttp.WithLabelFromCtx("http_version", func(ctx context.Context) string {
108
if v := ctx.Value(httpVersionKey); v != nil {
109
if versions, ok := v.([]string); ok {
110
if len(versions) > 0 {
111
return versions[0]
112
}
113
}
114
}
115
return "unknown"
116
})
117
}
118
119
func instrumentClientMetrics(transport http.RoundTripper) http.RoundTripper {
120
return promhttp.InstrumentRoundTripperCounter(clientMetrics.requestsTotal,
121
promhttp.InstrumentRoundTripperDuration(clientMetrics.requestsDuration,
122
transport,
123
withResourceLabel(),
124
withHttpVersionLabel(),
125
),
126
withResourceLabel(),
127
withHttpVersionLabel(),
128
)
129
}
130
131
func instrumentServerMetrics(next http.Handler) http.Handler {
132
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
133
next.ServeHTTP(w, req)
134
if v := req.Context().Value(resourceKey); v != nil {
135
if resources, ok := v.([]string); ok {
136
if len(resources) > 0 {
137
resources[0] = getHandlerResource(req)
138
}
139
}
140
}
141
if v := req.Context().Value(httpVersionKey); v != nil {
142
if versions, ok := v.([]string); ok {
143
if len(versions) > 0 {
144
versions[0] = req.Proto
145
}
146
}
147
}
148
})
149
instrumented := promhttp.InstrumentHandlerCounter(serverMetrics.requestsTotal,
150
promhttp.InstrumentHandlerDuration(serverMetrics.requestsDuration,
151
handler,
152
withResourceLabel(),
153
withHttpVersionLabel(),
154
),
155
withResourceLabel(),
156
withHttpVersionLabel(),
157
)
158
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
159
ctx := context.WithValue(req.Context(), resourceKey, []string{"unknown"})
160
ctx = context.WithValue(ctx, httpVersionKey, []string{"unknown"})
161
instrumented.ServeHTTP(w, req.WithContext(ctx))
162
})
163
}
164
165
func getHandlerResource(req *http.Request) string {
166
hostPart := getResourceHost(req)
167
if hostPart == "" {
168
hostPart = "unknown"
169
log.WithField("URL", req.URL).Warn("client metrics: cannot determine resource host part")
170
}
171
172
routePart := ""
173
if route := mux.CurrentRoute(req); route != nil {
174
routePart = route.GetName()
175
}
176
if routePart == "" {
177
log.WithField("URL", req.URL).Warn("client metrics: cannot determine resource route part")
178
routePart = "unknown"
179
}
180
if routePart == "root" {
181
routePart = ""
182
} else {
183
routePart = "/" + routePart
184
}
185
return hostPart + routePart
186
}
187
188
func getResourceHost(req *http.Request) string {
189
coords := getWorkspaceCoords(req)
190
191
var parts []string
192
193
if coords.Foreign {
194
parts = append(parts, "foreign_content")
195
}
196
197
if coords.ID != "" {
198
workspacePart := "workspace"
199
if coords.Debug {
200
workspacePart = "debug_" + workspacePart
201
}
202
if coords.Port != "" {
203
workspacePart += "_port"
204
}
205
parts = append(parts, workspacePart)
206
}
207
return strings.Join(parts, "/")
208
}
209
210