Skip to content

Commit 6c588c3

Browse files
author
Han Kang
authored
fix a number of unbounded dimensions in request metrics (kubernetes#89451)
* fix a number of unbounded dimensions in request metrics * add test suite for cleanVerb and cleanContentType * Properly validate that the content-type and charset (if applicable) are RFC compliant * add additional test case * truncate list of content-types Change-Id: Ia5fe0d2e2c602e4def4b8e0849cc19f3f9251818
1 parent c8ceeed commit 6c588c3

File tree

4 files changed

+219
-6
lines changed

4 files changed

+219
-6
lines changed

staging/src/k8s.io/apiserver/pkg/endpoints/metrics/BUILD

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"])
33
load(
44
"@io_bazel_rules_go//go:def.bzl",
55
"go_library",
6+
"go_test",
67
)
78

89
go_library(
@@ -35,3 +36,9 @@ filegroup(
3536
srcs = [":package-srcs"],
3637
tags = ["automanaged"],
3738
)
39+
40+
go_test(
41+
name = "go_default_test",
42+
srcs = ["metrics_test.go"],
43+
embed = [":go_default_library"],
44+
)

staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
"time"
2929

3030
restful "github.com/emicklei/go-restful"
31-
3231
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
3332
"k8s.io/apimachinery/pkg/types"
3433
utilsets "k8s.io/apimachinery/pkg/util/sets"
@@ -48,6 +47,8 @@ type resettableCollector interface {
4847

4948
const (
5049
APIServerComponent string = "apiserver"
50+
OtherContentType string = "other"
51+
OtherRequestMethod string = "other"
5152
)
5253

5354
/*
@@ -172,6 +173,37 @@ var (
172173
currentInflightRequests,
173174
requestTerminationsTotal,
174175
}
176+
177+
// these are the known (e.g. whitelisted/known) content types which we will report for
178+
// request metrics. Any other RFC compliant content types will be aggregated under 'unknown'
179+
knownMetricContentTypes = utilsets.NewString(
180+
"application/apply-patch+yaml",
181+
"application/json",
182+
"application/json-patch+json",
183+
"application/merge-patch+json",
184+
"application/strategic-merge-patch+json",
185+
"application/vnd.kubernetes.protobuf",
186+
"application/vnd.kubernetes.protobuf;stream=watch",
187+
"application/yaml",
188+
"text/plain",
189+
"text/plain;charset=utf-8")
190+
// these are the valid request methods which we report in our metrics. Any other request methods
191+
// will be aggregated under 'unknown'
192+
validRequestMethods = utilsets.NewString(
193+
"APPLY",
194+
"CONNECT",
195+
"CREATE",
196+
"DELETE",
197+
"DELETECOLLECTION",
198+
"GET",
199+
"LIST",
200+
"PATCH",
201+
"POST",
202+
"PROXY",
203+
"PUT",
204+
"UPDATE",
205+
"WATCH",
206+
"WATCHLIST")
175207
)
176208

177209
const (
@@ -219,6 +251,10 @@ func RecordRequestTermination(req *http.Request, requestInfo *request.RequestInf
219251
// translated to RequestInfo).
220252
// However, we need to tweak it e.g. to differentiate GET from LIST.
221253
verb := canonicalVerb(strings.ToUpper(req.Method), scope)
254+
// set verbs to a bounded set of known and expected verbs
255+
if !validRequestMethods.Has(verb) {
256+
verb = OtherRequestMethod
257+
}
222258
if requestInfo.IsResourceRequest {
223259
requestTerminationsTotal.WithLabelValues(cleanVerb(verb, req), requestInfo.APIGroup, requestInfo.APIVersion, requestInfo.Resource, requestInfo.Subresource, scope, component, codeToString(code)).Inc()
224260
} else {
@@ -256,7 +292,8 @@ func MonitorRequest(req *http.Request, verb, group, version, resource, subresour
256292
reportedVerb := cleanVerb(verb, req)
257293
dryRun := cleanDryRun(req.URL)
258294
elapsedSeconds := elapsed.Seconds()
259-
requestCounter.WithLabelValues(reportedVerb, dryRun, group, version, resource, subresource, scope, component, contentType, codeToString(httpCode)).Inc()
295+
cleanContentType := cleanContentType(contentType)
296+
requestCounter.WithLabelValues(reportedVerb, dryRun, group, version, resource, subresource, scope, component, cleanContentType, codeToString(httpCode)).Inc()
260297
requestLatencies.WithLabelValues(reportedVerb, dryRun, group, version, resource, subresource, scope, component).Observe(elapsedSeconds)
261298
// We are only interested in response sizes of read requests.
262299
if verb == "GET" || verb == "LIST" {
@@ -311,6 +348,19 @@ func InstrumentHandlerFunc(verb, group, version, resource, subresource, scope, c
311348
}
312349
}
313350

351+
// cleanContentType binds the contentType (for metrics related purposes) to a
352+
// bounded set of known/expected content-types.
353+
func cleanContentType(contentType string) string {
354+
normalizedContentType := strings.ToLower(contentType)
355+
if strings.HasSuffix(contentType, " stream=watch") || strings.HasSuffix(contentType, " charset=utf-8") {
356+
normalizedContentType = strings.ReplaceAll(contentType, " ", "")
357+
}
358+
if knownMetricContentTypes.Has(normalizedContentType) {
359+
return normalizedContentType
360+
}
361+
return OtherContentType
362+
}
363+
314364
// CleanScope returns the scope of the request.
315365
func CleanScope(requestInfo *request.RequestInfo) string {
316366
if requestInfo.Namespace != "" {
@@ -355,7 +405,10 @@ func cleanVerb(verb string, request *http.Request) string {
355405
if verb == "PATCH" && request.Header.Get("Content-Type") == string(types.ApplyPatchType) && utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
356406
reportedVerb = "APPLY"
357407
}
358-
return reportedVerb
408+
if validRequestMethods.Has(reportedVerb) {
409+
return reportedVerb
410+
}
411+
return OtherRequestMethod
359412
}
360413

361414
func cleanDryRun(u *url.URL) string {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package metrics
18+
19+
import (
20+
"fmt"
21+
"net/http"
22+
"net/url"
23+
"testing"
24+
)
25+
26+
func TestCleanVerb(t *testing.T) {
27+
testCases := []struct {
28+
desc string
29+
initialVerb string
30+
request *http.Request
31+
expectedVerb string
32+
}{
33+
{
34+
desc: "An empty string should be designated as unknown",
35+
initialVerb: "",
36+
request: nil,
37+
expectedVerb: "other",
38+
},
39+
{
40+
desc: "LIST should normally map to LIST",
41+
initialVerb: "LIST",
42+
request: nil,
43+
expectedVerb: "LIST",
44+
},
45+
{
46+
desc: "LIST should be transformed to WATCH if we have the right query param on the request",
47+
initialVerb: "LIST",
48+
request: &http.Request{
49+
URL: &url.URL{
50+
RawQuery: "watch=true",
51+
},
52+
},
53+
expectedVerb: "WATCH",
54+
},
55+
{
56+
desc: "LIST isn't transformed to WATCH if we have query params that do not include watch",
57+
initialVerb: "LIST",
58+
request: &http.Request{
59+
URL: &url.URL{
60+
RawQuery: "blah=asdf&something=else",
61+
},
62+
},
63+
expectedVerb: "LIST",
64+
},
65+
{
66+
desc: "WATCHLIST should be transformed to WATCH",
67+
initialVerb: "WATCHLIST",
68+
request: nil,
69+
expectedVerb: "WATCH",
70+
},
71+
{
72+
desc: "PATCH should be transformed to APPLY with the right content type",
73+
initialVerb: "PATCH",
74+
request: &http.Request{
75+
Header: http.Header{
76+
"Content-Type": []string{"application/apply-patch+yaml"},
77+
},
78+
},
79+
expectedVerb: "APPLY",
80+
},
81+
{
82+
desc: "PATCH shouldn't be transformed to APPLY without the right content type",
83+
initialVerb: "PATCH",
84+
request: nil,
85+
expectedVerb: "PATCH",
86+
},
87+
{
88+
desc: "WATCHLIST should be transformed to WATCH",
89+
initialVerb: "WATCHLIST",
90+
request: nil,
91+
expectedVerb: "WATCH",
92+
},
93+
{
94+
desc: "unexpected verbs should be designated as unknown",
95+
initialVerb: "notValid",
96+
request: nil,
97+
expectedVerb: "other",
98+
},
99+
}
100+
for _, tt := range testCases {
101+
t.Run(tt.initialVerb, func(t *testing.T) {
102+
req := &http.Request{URL: &url.URL{}}
103+
if tt.request != nil {
104+
req = tt.request
105+
}
106+
cleansedVerb := cleanVerb(tt.initialVerb, req)
107+
if cleansedVerb != tt.expectedVerb {
108+
t.Errorf("Got %s, but expected %s", cleansedVerb, tt.expectedVerb)
109+
}
110+
})
111+
}
112+
}
113+
114+
func TestContentType(t *testing.T) {
115+
testCases := []struct {
116+
rawContentType string
117+
expectedContentType string
118+
}{
119+
{
120+
rawContentType: "application/json",
121+
expectedContentType: "application/json",
122+
},
123+
{
124+
rawContentType: "image/svg+xml",
125+
expectedContentType: "other",
126+
},
127+
{
128+
rawContentType: "text/plain; charset=utf-8",
129+
expectedContentType: "text/plain;charset=utf-8",
130+
},
131+
{
132+
rawContentType: "application/json;foo=bar",
133+
expectedContentType: "other",
134+
},
135+
{
136+
rawContentType: "application/json;charset=hancoding",
137+
expectedContentType: "other",
138+
},
139+
{
140+
rawContentType: "unknownbutvalidtype",
141+
expectedContentType: "other",
142+
},
143+
}
144+
145+
for _, tt := range testCases {
146+
t.Run(fmt.Sprintf("parse %s", tt.rawContentType), func(t *testing.T) {
147+
cleansedContentType := cleanContentType(tt.rawContentType)
148+
if cleansedContentType != tt.expectedContentType {
149+
t.Errorf("Got %s, but expected %s", cleansedContentType, tt.expectedContentType)
150+
}
151+
})
152+
}
153+
}

staging/src/k8s.io/apiserver/pkg/server/healthz/healthz_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,9 @@ func TestMetrics(t *testing.T) {
256256
expected := strings.NewReader(`
257257
# HELP apiserver_request_total [ALPHA] Counter of apiserver requests broken out for each verb, dry run value, group, version, resource, scope, component, and HTTP response contentType and code.
258258
# TYPE apiserver_request_total counter
259-
apiserver_request_total{code="200",component="",contentType="text/plain; charset=utf-8",dry_run="",group="",resource="",scope="",subresource="/healthz",verb="GET",version=""} 1
260-
apiserver_request_total{code="200",component="",contentType="text/plain; charset=utf-8",dry_run="",group="",resource="",scope="",subresource="/livez",verb="GET",version=""} 1
261-
apiserver_request_total{code="200",component="",contentType="text/plain; charset=utf-8",dry_run="",group="",resource="",scope="",subresource="/readyz",verb="GET",version=""} 1
259+
apiserver_request_total{code="200",component="",contentType="text/plain;charset=utf-8",dry_run="",group="",resource="",scope="",subresource="/healthz",verb="GET",version=""} 1
260+
apiserver_request_total{code="200",component="",contentType="text/plain;charset=utf-8",dry_run="",group="",resource="",scope="",subresource="/livez",verb="GET",version=""} 1
261+
apiserver_request_total{code="200",component="",contentType="text/plain;charset=utf-8",dry_run="",group="",resource="",scope="",subresource="/readyz",verb="GET",version=""} 1
262262
`)
263263
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, expected, "apiserver_request_total"); err != nil {
264264
t.Error(err)

0 commit comments

Comments
 (0)