From 0c432ccf58b968040bcf6009bc255eeff0092f5a Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 29 Mar 2022 18:38:58 +0000 Subject: [PATCH] Track field validation in metrics --- .../pkg/endpoints/metrics/metrics.go | 33 +++++++++++ .../pkg/endpoints/metrics/metrics_test.go | 56 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go b/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go index 2a612e02150..671fb02c5b4 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go @@ -122,6 +122,19 @@ var ( }, []string{"verb", "group", "version", "resource", "subresource", "scope", "component"}, ) + fieldValidationRequestLatencies = compbasemetrics.NewHistogramVec( + &compbasemetrics.HistogramOpts{ + Name: "field_validation_request_duration_seconds", + Help: "Response latency distribution in seconds for each field validation value and whether field validation is enabled or not", + // This metric is supplementary to the requestLatencies metric. + // It measures request durations for the various field validation + // values. + Buckets: []float64{0.05, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0, 1.25, 1.5, 2, 3, + 4, 5, 6, 8, 10, 15, 20, 30, 45, 60}, + StabilityLevel: compbasemetrics.ALPHA, + }, + []string{"field_validation", "enabled"}, + ) responseSizes = compbasemetrics.NewHistogramVec( &compbasemetrics.HistogramOpts{ Name: "apiserver_response_sizes", @@ -261,6 +274,7 @@ var ( longRunningRequestGauge, requestLatencies, requestSloLatencies, + fieldValidationRequestLatencies, responseSizes, droppedRequests, TLSHandshakeErrors, @@ -510,6 +524,10 @@ func MonitorRequest(req *http.Request, verb, group, version, resource, subresour } } requestLatencies.WithContext(req.Context()).WithLabelValues(reportedVerb, dryRun, group, version, resource, subresource, scope, component).Observe(elapsedSeconds) + fieldValidation := cleanFieldValidation(req.URL) + fieldValidationEnabled := strconv.FormatBool(utilfeature.DefaultFeatureGate.Enabled(features.ServerSideFieldValidation)) + fieldValidationRequestLatencies.WithContext(req.Context()).WithLabelValues(fieldValidation, fieldValidationEnabled) + if wd, ok := request.LatencyTrackersFrom(req.Context()); ok { sloLatency := elapsedSeconds - (wd.MutatingWebhookTracker.GetLatency() + wd.ValidatingWebhookTracker.GetLatency()).Seconds() requestSloLatencies.WithContext(req.Context()).WithLabelValues(reportedVerb, group, version, resource, subresource, scope, component).Observe(sloLatency) @@ -650,6 +668,21 @@ func cleanDryRun(u *url.URL) string { return strings.Join(utilsets.NewString(dryRun...).List(), ",") } +func cleanFieldValidation(u *url.URL) string { + // avoid allocating when we don't see dryRun in the query + if !strings.Contains(u.RawQuery, "fieldValidation") { + return "" + } + fieldValidation := u.Query()["fieldValidation"] + if len(fieldValidation) != 1 { + return "invalid" + } + if errs := validation.ValidateFieldValidation(nil, fieldValidation[0]); len(errs) > 0 { + return "invalid" + } + return fieldValidation[0] +} + var _ http.ResponseWriter = (*ResponseWriterDelegator)(nil) var _ responsewriter.UserProvidedDecorator = (*ResponseWriterDelegator)(nil) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics_test.go index 8403f1c3ad9..6e8ac564a41 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics_test.go @@ -204,6 +204,62 @@ func TestCleanScope(t *testing.T) { } } +func TestCleanFieldValidation(t *testing.T) { + testCases := []struct { + name string + url *url.URL + expectedFieldValidation string + }{ + { + name: "empty field validation", + url: &url.URL{}, + expectedFieldValidation: "", + }, + { + name: "ignore field validation", + url: &url.URL{ + RawQuery: "fieldValidation=Ignore", + }, + expectedFieldValidation: "Ignore", + }, + { + name: "warn field validation", + url: &url.URL{ + RawQuery: "fieldValidation=Warn", + }, + expectedFieldValidation: "Warn", + }, + { + name: "strict field validation", + url: &url.URL{ + RawQuery: "fieldValidation=Strict", + }, + expectedFieldValidation: "Strict", + }, + { + name: "invalid field validation", + url: &url.URL{ + RawQuery: "fieldValidation=foo", + }, + expectedFieldValidation: "invalid", + }, + { + name: "multiple field validation", + url: &url.URL{ + RawQuery: "fieldValidation=Strict&fieldValidation=Ignore", + }, + expectedFieldValidation: "invalid", + }, + } + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + if fieldValidation := cleanFieldValidation(test.url); fieldValidation != test.expectedFieldValidation { + t.Errorf("failed to clean field validation, expected: %s, got: %s", test.expectedFieldValidation, fieldValidation) + } + }) + } +} + func TestResponseWriterDecorator(t *testing.T) { decorator := &ResponseWriterDelegator{ ResponseWriter: &responsewriter.FakeResponseWriter{},