runsc metric-server: Rewrite /metrics requests to support URL query params

Prometheus's `scrape_config.metrics_path` setting allows customizing the
URL to scrape from its default of `/metrics`.

`runsc metric-server` supports optional `GET` URL query parameters to filter
the list of returned metrics, such that the scraping client can express which
metrics it's interested in to the metric server.

However, Prometheus's `scrape_config.metrics_path` encodes any URL-special
character to percent-encoding form, i.e. `?` becomes `%3F`. This is
interpreted by Go's HTTP server implementation as part of the URL path
component rather than a delimiter for the query parameters.

This CL makes `runsc metric-server` handle this case by internally rewriting
the URL back to `/metrics?query_string_here`.

While somewhat hacky, this makes Prometheus integration easier, and the
behavior of serving metrics data for URLs starting with `/metrics%3F` doesn't
seem like it would be surprising.

PiperOrigin-RevId: 523834900
This commit is contained in:
Etienne Perot
2023-04-12 16:23:02 -07:00
committed by gVisor bot
parent e0b1585586
commit f515471c3f
3 changed files with 43 additions and 1 deletions
+10
View File
@@ -578,6 +578,16 @@ var httpOK = httpResult{code: http.StatusOK}
// serveIndex serves the index page.
func (m *MetricServer) serveIndex(w http.ResponseWriter, req *http.Request) httpResult {
if req.URL.Path != "/" {
if strings.HasPrefix(req.URL.Path, "/metrics?") {
// Prometheus's scrape_config.metrics_path takes in a query path and automatically encodes
// all special characters in it to %-form, including the "?" character.
// This can prevent use of query parameters, and we end up here instead.
// To address this, rewrite the URL to undo this transformation.
// This means requesting "/metrics%3Ffoo=bar" is rewritten to "/metrics?foo=bar".
req.URL.RawQuery = strings.TrimPrefix(req.URL.Path, "/metrics?")
req.URL.Path = "/metrics"
return m.serveMetrics(w, req)
}
return httpResult{http.StatusNotFound, errors.New("path not found")}
}
fmt.Fprintf(w, "<html><head><title>runsc metrics</title></head><body>")
+24
View File
@@ -647,6 +647,30 @@ func TestContainerMetricsFilter(t *testing.T) {
if err != nil {
t.Errorf("Cannot get sandbox metadata from unfiltered data: %v", err)
}
// Fifth pass: Use alternate URL encoding to mimic Prometheus's URL-encoding
// behavior.
alternatePathData, err := te.client.GetMetrics(te.testCtx, map[string]string{
// Encoded version of "/metrics?runsc-sandbox-metrics-filter=^$", this should match nothing.
"": "/metrics%3Frunsc-sandbox-metrics-filter=%5E%24",
})
if err != nil {
t.Fatalf("Cannot get metrics: %v", err)
}
_, err = alternatePathData.GetSandboxMetadataMetric(metricclient.WantMetric{
Metric: "testmetric_meta_sandbox_metadata",
Sandbox: args.ID,
})
if err != nil {
t.Errorf("Cannot get sandbox metadata from data obtained from alternate path: %v\n\nData:\n\n%v\n\n", err, alternatePathData)
}
_, _, err = alternatePathData.GetPrometheusContainerInteger(metricclient.WantMetric{
Metric: "testmetric_fs_opens",
Sandbox: args.ID,
})
if err == nil {
t.Errorf("Was unexpectedly able to get testmetric_fs_opens from data obtained from alternate path which was supposed to filter it out:\n\n%v\n\n", alternatePathData)
}
}
// TestContainerCapabilityFilter verifies the ability to filter capabilities in /metrics requests.
+9 -1
View File
@@ -274,8 +274,16 @@ func (c *MetricClient) ShutdownServer(ctx context.Context) error {
type MetricData string
// GetMetrics returns the raw Prometheus-formatted metric data from the metric server.
// `urlParams` may contain a special parameter with the empty string as the key.
// If this is set, that string is used to override the request path from its default
// value of `/metrics`.
func (c *MetricClient) GetMetrics(ctx context.Context, urlParams map[string]string) (MetricData, error) {
resp, closeReq, err := c.req(ctx, 10*time.Second, http.MethodGet, "/metrics", urlParams)
path := "/metrics"
if overridePath, found := urlParams[""]; found {
path = overridePath
delete(urlParams, "")
}
resp, closeReq, err := c.req(ctx, 10*time.Second, http.MethodGet, path, urlParams)
if err != nil {
return "", fmt.Errorf("cannot get /metrics: %v", err)
}