Skip to content

Commit 2be52da

Browse files
kylos101akosyakov
andauthored
[ws-proxy] introduce RED metrics, including a http_version label (#20196)
* Introduce RED metrics for ws-proxy Originally from #17294 Co-authored-by: Anton Kosyakov <[email protected]> * Remove unused var * [ws-proxy] fix crash loop backoff (WIP) I think for this value to be populated, we'll need to "bubble up" httpVersion (like what was done with many methods and resource) 🤔 Think of a better way. * Add namespace and subsystem to metrics * Set a value for http_version label * Persist http_version for server metrics * Code review feedback --------- Co-authored-by: Anton Kosyakov <[email protected]>
1 parent 35f53b9 commit 2be52da

File tree

5 files changed

+269
-34
lines changed

5 files changed

+269
-34
lines changed

components/ws-proxy/pkg/common/infoprovider.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const (
2323
WorkspacePathPrefixIdentifier = "workspacePathPrefix"
2424

2525
WorkspaceInfoIdentifier = "workspaceInfo"
26+
27+
ForeignContentIdentifier = "foreignContent"
2628
)
2729

2830
// WorkspaceCoords represents the coordinates of a workspace (port).
@@ -33,6 +35,8 @@ type WorkspaceCoords struct {
3335
Port string
3436
// Debug workspace
3537
Debug bool
38+
// Foreign content
39+
Foreign bool
3640
}
3741

3842
// WorkspaceInfoProvider is an entity that is able to provide workspaces related information.
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
}

components/ws-proxy/pkg/proxy/pass.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ type proxyPassOpt func(h *proxyPassConfig)
4141
type errorHandler func(http.ResponseWriter, *http.Request, error)
4242

4343
// targetResolver is a function that determines to which target to forward the given HTTP request to.
44-
type targetResolver func(*Config, common.WorkspaceInfoProvider, *http.Request) (*url.URL, error)
44+
type targetResolver func(*Config, common.WorkspaceInfoProvider, *http.Request) (*url.URL, string, error)
4545

4646
type responseHandler func(*http.Response, *http.Request) error
4747

@@ -119,7 +119,7 @@ func proxyPass(config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProv
119119
}
120120

121121
return func(w http.ResponseWriter, req *http.Request) {
122-
targetURL, err := h.TargetResolver(config.Config, infoProvider, req)
122+
targetURL, targetResource, err := h.TargetResolver(config.Config, infoProvider, req)
123123
if err != nil {
124124
if h.ErrorHandler != nil {
125125
h.ErrorHandler(w, req, err)
@@ -128,6 +128,8 @@ func proxyPass(config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProv
128128
}
129129
return
130130
}
131+
req = withResourceMetricsLabel(req, targetResource)
132+
req = withHttpVersionMetricsLabel(req)
131133

132134
originalURL := *req.URL
133135

@@ -216,10 +218,10 @@ func withErrorHandler(h errorHandler) proxyPassOpt {
216218
}
217219
}
218220

219-
func createDefaultTransport(config *TransportConfig) *http.Transport {
221+
func createDefaultTransport(config *TransportConfig) http.RoundTripper {
220222
// TODO equivalent of client_max_body_size 2048m; necessary ???
221223
// this is based on http.DefaultTransport, with some values exposed to config
222-
return &http.Transport{
224+
return instrumentClientMetrics(&http.Transport{
223225
Proxy: http.ProxyFromEnvironment,
224226
DialContext: (&net.Dialer{
225227
Timeout: time.Duration(config.ConnectTimeout), // default: 30s
@@ -232,7 +234,7 @@ func createDefaultTransport(config *TransportConfig) *http.Transport {
232234
IdleConnTimeout: time.Duration(config.IdleConnTimeout), // default: 90s
233235
TLSHandshakeTimeout: 10 * time.Second,
234236
ExpectContinueTimeout: 1 * time.Second,
235-
}
237+
})
236238
}
237239

238240
// tell the browser to cache for 1 year and don't ask the server during this period.

0 commit comments

Comments
 (0)