Skip to content

Commit 07f0d43

Browse files
Merge pull request #115630 from Jefftree/agg-discovery-metrics
Add metrics for aggregated discovery Kubernetes-commit: 2e3c5003b96aef29e87ee24c9086ff7f06cb8886
2 parents 4bde4d7 + 31f3fb3 commit 07f0d43

File tree

6 files changed

+167
-21
lines changed

6 files changed

+167
-21
lines changed

pkg/endpoints/discovery/aggregated/etag.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func ServeHTTPWithETag(
5454
// Otherwise, we delegate to the handler for actual content
5555
//
5656
// According to documentation, An Etag within an If-None-Match
57-
// header will be enclosed within doule quotes:
57+
// header will be enclosed within double quotes:
5858
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
5959
if clientCachedHash := req.Header.Get("If-None-Match"); quotedHash == clientCachedHash {
6060
w.WriteHeader(http.StatusNotModified)

pkg/endpoints/discovery/aggregated/handler.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"k8s.io/apimachinery/pkg/runtime/serializer"
2727
"k8s.io/apimachinery/pkg/version"
2828
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
29+
"k8s.io/apiserver/pkg/endpoints/metrics"
2930

3031
"sync/atomic"
3132

@@ -72,6 +73,8 @@ type resourceDiscoveryManager struct {
7273
// cache is an atomic pointer to avoid the use of locks
7374
cache atomic.Pointer[cachedGroupList]
7475

76+
serveHTTPFunc http.HandlerFunc
77+
7578
// Writes protected by the lock.
7679
// List of all apigroups & resources indexed by the resource manager
7780
lock sync.RWMutex
@@ -84,13 +87,26 @@ type priorityInfo struct {
8487
VersionPriority int
8588
}
8689

87-
func NewResourceManager() ResourceManager {
90+
func NewResourceManager(path string) ResourceManager {
8891
scheme := runtime.NewScheme()
8992
codecs := serializer.NewCodecFactory(scheme)
9093
utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme))
91-
return &resourceDiscoveryManager{serializer: codecs, versionPriorities: make(map[metav1.GroupVersion]priorityInfo)}
94+
rdm := &resourceDiscoveryManager{
95+
serializer: codecs,
96+
versionPriorities: make(map[metav1.GroupVersion]priorityInfo),
97+
}
98+
rdm.serveHTTPFunc = metrics.InstrumentHandlerFunc("GET",
99+
/* group = */ "",
100+
/* version = */ "",
101+
/* resource = */ "",
102+
/* subresource = */ path,
103+
/* scope = */ "",
104+
/* component = */ metrics.APIServerComponent,
105+
/* deprecated */ false,
106+
/* removedRelease */ "",
107+
rdm.serveHTTP)
108+
return rdm
92109
}
93-
94110
func (rdm *resourceDiscoveryManager) SetGroupVersionPriority(gv metav1.GroupVersion, groupPriorityMinimum, versionPriority int) {
95111
rdm.lock.Lock()
96112
defer rdm.lock.Unlock()
@@ -246,6 +262,7 @@ func (rdm *resourceDiscoveryManager) RemoveGroup(groupName string) {
246262
// Prepares the api group list for serving by converting them from map into
247263
// list and sorting them according to insertion order
248264
func (rdm *resourceDiscoveryManager) calculateAPIGroupsLocked() []apidiscoveryv2beta1.APIGroupDiscovery {
265+
regenerationCounter.Inc()
249266
// Re-order the apiGroups by their priority.
250267
groups := []apidiscoveryv2beta1.APIGroupDiscovery{}
251268
for _, group := range rdm.apiGroups {
@@ -338,6 +355,10 @@ type cachedGroupList struct {
338355
}
339356

340357
func (rdm *resourceDiscoveryManager) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
358+
rdm.serveHTTPFunc(resp, req)
359+
}
360+
361+
func (rdm *resourceDiscoveryManager) serveHTTP(resp http.ResponseWriter, req *http.Request) {
341362
cache := rdm.fetchFromCache()
342363
response := cache.cachedResponse
343364
etag := cache.cachedResponseETag

pkg/endpoints/discovery/aggregated/handler_test.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func fetchPath(handler http.Handler, acceptPrefix string, path string, etag stri
120120

121121
// Add all builtin APIServices to the manager and check the output
122122
func TestBasicResponse(t *testing.T) {
123-
manager := discoveryendpoint.NewResourceManager()
123+
manager := discoveryendpoint.NewResourceManager("apis")
124124

125125
apis := fuzzAPIGroups(1, 3, 10)
126126
manager.SetGroups(apis.Items)
@@ -141,7 +141,7 @@ func TestBasicResponse(t *testing.T) {
141141

142142
// Test that protobuf is outputted correctly
143143
func TestBasicResponseProtobuf(t *testing.T) {
144-
manager := discoveryendpoint.NewResourceManager()
144+
manager := discoveryendpoint.NewResourceManager("apis")
145145

146146
apis := fuzzAPIGroups(1, 3, 10)
147147
manager.SetGroups(apis.Items)
@@ -157,8 +157,8 @@ func TestBasicResponseProtobuf(t *testing.T) {
157157
// e.g.: Multiple services with the same contents should have the same etag.
158158
func TestEtagConsistent(t *testing.T) {
159159
// Create 2 managers, add a bunch of services to each
160-
manager1 := discoveryendpoint.NewResourceManager()
161-
manager2 := discoveryendpoint.NewResourceManager()
160+
manager1 := discoveryendpoint.NewResourceManager("apis")
161+
manager2 := discoveryendpoint.NewResourceManager("apis")
162162

163163
apis := fuzzAPIGroups(1, 3, 11)
164164
manager1.SetGroups(apis.Items)
@@ -231,7 +231,7 @@ func TestEtagConsistent(t *testing.T) {
231231
// Test that if a request comes in with an If-None-Match header with an incorrect
232232
// E-Tag, that fresh content is returned.
233233
func TestEtagNonMatching(t *testing.T) {
234-
manager := discoveryendpoint.NewResourceManager()
234+
manager := discoveryendpoint.NewResourceManager("apis")
235235
apis := fuzzAPIGroups(1, 3, 12)
236236
manager.SetGroups(apis.Items)
237237

@@ -251,7 +251,7 @@ func TestEtagNonMatching(t *testing.T) {
251251
// Test that if a request comes in with an If-None-Match header with a correct
252252
// E-Tag, that 304 Not Modified is returned
253253
func TestEtagMatching(t *testing.T) {
254-
manager := discoveryendpoint.NewResourceManager()
254+
manager := discoveryendpoint.NewResourceManager("apis")
255255
apis := fuzzAPIGroups(1, 3, 12)
256256
manager.SetGroups(apis.Items)
257257

@@ -273,7 +273,7 @@ func TestEtagMatching(t *testing.T) {
273273
// Test that if a request comes in with an If-None-Match header with an old
274274
// E-Tag, that fresh content is returned
275275
func TestEtagOutdated(t *testing.T) {
276-
manager := discoveryendpoint.NewResourceManager()
276+
manager := discoveryendpoint.NewResourceManager("apis")
277277
apis := fuzzAPIGroups(1, 3, 15)
278278
manager.SetGroups(apis.Items)
279279

@@ -301,7 +301,7 @@ func TestEtagOutdated(t *testing.T) {
301301

302302
// Test that an api service can be added or removed
303303
func TestAddRemove(t *testing.T) {
304-
manager := discoveryendpoint.NewResourceManager()
304+
manager := discoveryendpoint.NewResourceManager("apis")
305305
apis := fuzzAPIGroups(1, 3, 15)
306306
for _, group := range apis.Items {
307307
for _, version := range group.Versions {
@@ -331,7 +331,7 @@ func TestAddRemove(t *testing.T) {
331331
// Show that updating an existing service replaces and does not add the entry
332332
// and instead replaces it
333333
func TestUpdateService(t *testing.T) {
334-
manager := discoveryendpoint.NewResourceManager()
334+
manager := discoveryendpoint.NewResourceManager("apis")
335335
apis := fuzzAPIGroups(1, 3, 15)
336336
for _, group := range apis.Items {
337337
for _, version := range group.Versions {
@@ -368,7 +368,7 @@ func TestUpdateService(t *testing.T) {
368368
// Show the discovery manager is capable of serving requests to multiple users
369369
// with unchanging data
370370
func TestConcurrentRequests(t *testing.T) {
371-
manager := discoveryendpoint.NewResourceManager()
371+
manager := discoveryendpoint.NewResourceManager("apis")
372372
apis := fuzzAPIGroups(1, 3, 15)
373373
manager.SetGroups(apis.Items)
374374

@@ -410,7 +410,7 @@ func TestConcurrentRequests(t *testing.T) {
410410
// concurrent writers without tripping up. Good to run with go '-race' detector
411411
// since there are not many "correctness" checks
412412
func TestAbuse(t *testing.T) {
413-
manager := discoveryendpoint.NewResourceManager()
413+
manager := discoveryendpoint.NewResourceManager("apis")
414414

415415
numReaders := 100
416416
numRequestsPerReader := 1000
@@ -505,7 +505,7 @@ func TestAbuse(t *testing.T) {
505505
}
506506

507507
func TestVersionSortingNoPriority(t *testing.T) {
508-
manager := discoveryendpoint.NewResourceManager()
508+
manager := discoveryendpoint.NewResourceManager("apis")
509509

510510
manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{
511511
Version: "v1alpha1",
@@ -537,7 +537,7 @@ func TestVersionSortingNoPriority(t *testing.T) {
537537
}
538538

539539
func TestVersionSortingWithPriority(t *testing.T) {
540-
manager := discoveryendpoint.NewResourceManager()
540+
manager := discoveryendpoint.NewResourceManager("apis")
541541

542542
manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{
543543
Version: "v1",
@@ -560,7 +560,7 @@ func TestVersionSortingWithPriority(t *testing.T) {
560560

561561
// if two apiservices declare conflicting priorities for their group priority, take the higher one.
562562
func TestGroupVersionSortingConflictingPriority(t *testing.T) {
563-
manager := discoveryendpoint.NewResourceManager()
563+
manager := discoveryendpoint.NewResourceManager("apis")
564564

565565
manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{
566566
Version: "v1",
@@ -588,7 +588,7 @@ func TestGroupVersionSortingConflictingPriority(t *testing.T) {
588588
// Show that the GroupPriorityMinimum is not sticky if a higher group version is removed
589589
// after a lower one is added
590590
func TestStatelessGroupPriorityMinimum(t *testing.T) {
591-
manager := discoveryendpoint.NewResourceManager()
591+
manager := discoveryendpoint.NewResourceManager("apis")
592592

593593
stableGroup := "stable.example.com"
594594
experimentalGroup := "experimental.example.com"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2023 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 aggregated
18+
19+
import (
20+
"k8s.io/component-base/metrics"
21+
"k8s.io/component-base/metrics/legacyregistry"
22+
)
23+
24+
var (
25+
regenerationCounter = metrics.NewCounter(
26+
&metrics.CounterOpts{
27+
Name: "aggregator_discovery_aggregation_count_total",
28+
Help: "Counter of number of times discovery was aggregated",
29+
StabilityLevel: metrics.ALPHA,
30+
},
31+
)
32+
)
33+
34+
func init() {
35+
legacyregistry.MustRegister(regenerationCounter)
36+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
Copyright 2022 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 aggregated_test
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"strings"
23+
"testing"
24+
25+
"k8s.io/component-base/metrics/legacyregistry"
26+
"k8s.io/component-base/metrics/testutil"
27+
28+
discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
29+
)
30+
31+
func formatExpectedMetrics(aggregationCount int) io.Reader {
32+
expected := ``
33+
if aggregationCount > 0 {
34+
expected = expected + `# HELP aggregator_discovery_aggregation_count_total [ALPHA] Counter of number of times discovery was aggregated
35+
# TYPE aggregator_discovery_aggregation_count_total counter
36+
aggregator_discovery_aggregation_count_total %d
37+
`
38+
}
39+
args := []any{}
40+
if aggregationCount > 0 {
41+
args = append(args, aggregationCount)
42+
}
43+
return strings.NewReader(fmt.Sprintf(expected, args...))
44+
}
45+
46+
func TestBasicMetrics(t *testing.T) {
47+
legacyregistry.Reset()
48+
manager := discoveryendpoint.NewResourceManager("apis")
49+
50+
apis := fuzzAPIGroups(1, 3, 10)
51+
manager.SetGroups(apis.Items)
52+
53+
interests := []string{"aggregator_discovery_aggregation_count_total"}
54+
55+
_, _, _ = fetchPath(manager, "application/json", discoveryPath, "")
56+
// A single fetch should aggregate and increment regeneration counter.
57+
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(1), interests...); err != nil {
58+
t.Fatal(err)
59+
}
60+
_, _, _ = fetchPath(manager, "application/json", discoveryPath, "")
61+
// Subsequent fetches should not reaggregate discovery.
62+
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(1), interests...); err != nil {
63+
t.Fatal(err)
64+
}
65+
}
66+
67+
func TestMetricsModified(t *testing.T) {
68+
legacyregistry.Reset()
69+
manager := discoveryendpoint.NewResourceManager("apis")
70+
71+
apis := fuzzAPIGroups(1, 3, 10)
72+
manager.SetGroups(apis.Items)
73+
74+
interests := []string{"aggregator_discovery_aggregation_count_total"}
75+
76+
_, _, _ = fetchPath(manager, "application/json", discoveryPath, "")
77+
// A single fetch should aggregate and increment regeneration counter.
78+
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(1), interests...); err != nil {
79+
t.Fatal(err)
80+
}
81+
82+
// Update discovery document.
83+
manager.SetGroups(fuzzAPIGroups(1, 3, 10).Items)
84+
_, _, _ = fetchPath(manager, "application/json", discoveryPath, "")
85+
// If the discovery content has changed, reaggregation should be performed.
86+
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(2), interests...); err != nil {
87+
t.Fatal(err)
88+
}
89+
}

pkg/server/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,10 +738,10 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G
738738
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AggregatedDiscoveryEndpoint) {
739739
manager := c.AggregatedDiscoveryGroupManager
740740
if manager == nil {
741-
manager = discoveryendpoint.NewResourceManager()
741+
manager = discoveryendpoint.NewResourceManager("apis")
742742
}
743743
s.AggregatedDiscoveryGroupManager = manager
744-
s.AggregatedLegacyDiscoveryGroupManager = discoveryendpoint.NewResourceManager()
744+
s.AggregatedLegacyDiscoveryGroupManager = discoveryendpoint.NewResourceManager("api")
745745
}
746746
for {
747747
if c.JSONPatchMaxCopyBytes <= 0 {

0 commit comments

Comments
 (0)