Skip to content

Commit d74ab9e

Browse files
committed
Take into account latency incurred in server filters
apiserver_request_duration_seconds does not take into account the time a request spends in the server filters. If a filter takes longer then the latency incurred will not be reflected in the apiserver latency metrics. For example, the amount of time a request spends in priority and fairness machineries or in shuffle queues will not be accounted for. - Add a server filter that attaches request received timestamp to the request context very early in in the handler chain (as soon as net/http hands over control to us). - Use the above received timestamp in the apiserver latency metrics apiserver_request_duration_seconds. - Use the above received timestamp in the audit layer to set RequestReceivedTimestamp.
1 parent 57e9a41 commit d74ab9e

File tree

10 files changed

+236
-9
lines changed

10 files changed

+236
-9
lines changed

staging/src/k8s.io/apiserver/pkg/audit/request.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ const (
4343
userAgentTruncateSuffix = "...TRUNCATED"
4444
)
4545

46-
func NewEventFromRequest(req *http.Request, level auditinternal.Level, attribs authorizer.Attributes) (*auditinternal.Event, error) {
46+
func NewEventFromRequest(req *http.Request, requestReceivedTimestamp time.Time, level auditinternal.Level, attribs authorizer.Attributes) (*auditinternal.Event, error) {
4747
ev := &auditinternal.Event{
48-
RequestReceivedTimestamp: metav1.NewMicroTime(time.Now()),
48+
RequestReceivedTimestamp: metav1.NewMicroTime(requestReceivedTimestamp),
4949
Verb: attribs.GetVerb(),
5050
RequestURI: req.URL.RequestURI(),
5151
UserAgent: maybeTruncateUserAgent(req),

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ go_test(
1616
"cachecontrol_test.go",
1717
"impersonation_test.go",
1818
"metrics_test.go",
19+
"request_received_time_test.go",
1920
"requestinfo_test.go",
2021
"warning_test.go",
2122
],
@@ -27,6 +28,7 @@ go_test(
2728
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
2829
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
2930
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
31+
"//staging/src/k8s.io/apimachinery/pkg/util/clock:go_default_library",
3032
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
3133
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
3234
"//staging/src/k8s.io/apiserver/pkg/apis/audit:go_default_library",
@@ -54,6 +56,7 @@ go_library(
5456
"doc.go",
5557
"impersonation.go",
5658
"metrics.go",
59+
"request_received_time.go",
5760
"requestinfo.go",
5861
"warning.go",
5962
],
@@ -66,6 +69,7 @@ go_library(
6669
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
6770
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
6871
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
72+
"//staging/src/k8s.io/apimachinery/pkg/util/clock:go_default_library",
6973
"//staging/src/k8s.io/apimachinery/pkg/util/net:go_default_library",
7074
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
7175
"//staging/src/k8s.io/apiserver/pkg/apis/audit:go_default_library",

staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ func createAuditEventAndAttachToContext(req *http.Request, policy policy.Checker
131131
return req, nil, nil, nil
132132
}
133133

134-
ev, err := audit.NewEventFromRequest(req, level, attribs)
134+
requestReceivedTimestamp, ok := request.ReceivedTimestampFrom(ctx)
135+
if !ok {
136+
requestReceivedTimestamp = time.Now()
137+
}
138+
ev, err := audit.NewEventFromRequest(req, requestReceivedTimestamp, level, attribs)
135139
if err != nil {
136140
return req, nil, nil, fmt.Errorf("failed to complete audit event from request: %v", err)
137141
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright 2020 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 filters
18+
19+
import (
20+
"net/http"
21+
22+
utilclock "k8s.io/apimachinery/pkg/util/clock"
23+
"k8s.io/apiserver/pkg/endpoints/request"
24+
)
25+
26+
// WithRequestReceivedTimestamp attaches the ReceivedTimestamp (the time the request reached
27+
// the apiserver) to the context.
28+
func WithRequestReceivedTimestamp(handler http.Handler) http.Handler {
29+
return withRequestReceivedTimestampWithClock(handler, utilclock.RealClock{})
30+
}
31+
32+
// The clock is passed as a parameter, handy for unit testing.
33+
func withRequestReceivedTimestampWithClock(handler http.Handler, clock utilclock.PassiveClock) http.Handler {
34+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
35+
ctx := req.Context()
36+
req = req.WithContext(request.WithReceivedTimestamp(ctx, clock.Now()))
37+
38+
handler.ServeHTTP(w, req)
39+
})
40+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
Copyright 2020 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 filters
18+
19+
import (
20+
"net/http"
21+
"net/http/httptest"
22+
"testing"
23+
"time"
24+
25+
utilclock "k8s.io/apimachinery/pkg/util/clock"
26+
"k8s.io/apiserver/pkg/endpoints/request"
27+
)
28+
29+
func TestWithRequestReceivedTimestamp(t *testing.T) {
30+
receivedTimestampExpected := time.Now()
31+
32+
var (
33+
callCount int
34+
receivedTimestampGot time.Time
35+
ok bool
36+
)
37+
handler := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
38+
callCount++
39+
40+
// does the handler chain that follows see the request received timestamp?
41+
receivedTimestampGot, ok = request.ReceivedTimestampFrom(req.Context())
42+
})
43+
44+
wrapped := withRequestReceivedTimestampWithClock(handler, utilclock.NewFakeClock(receivedTimestampExpected))
45+
46+
testRequest, err := http.NewRequest(http.MethodGet, "/api/v1/namespaces", nil)
47+
if err != nil {
48+
t.Fatalf("failed to create new http request - %v", err)
49+
}
50+
51+
w := httptest.NewRecorder()
52+
wrapped.ServeHTTP(w, testRequest)
53+
54+
if callCount != 1 {
55+
t.Errorf("WithRequestReceivedTimestamp: expected the original handler to be invoked once, but was actually invoked %d times", callCount)
56+
}
57+
if !ok {
58+
t.Error("WithRequestReceivedTimestamp: expected request.ReceivedTimestampFrom to return true, but got false")
59+
}
60+
if receivedTimestampExpected != receivedTimestampGot {
61+
t.Errorf("WithRequestReceivedTimestamp: request received timestamp expected=%s but got=%s", receivedTimestampExpected, receivedTimestampGot)
62+
}
63+
}

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,11 @@ func MonitorRequest(req *http.Request, verb, group, version, resource, subresour
368368
// InstrumentRouteFunc works like Prometheus' InstrumentHandlerFunc but wraps
369369
// the go-restful RouteFunction instead of a HandlerFunc plus some Kubernetes endpoint specific information.
370370
func InstrumentRouteFunc(verb, group, version, resource, subresource, scope, component string, deprecated bool, removedRelease string, routeFunc restful.RouteFunction) restful.RouteFunction {
371-
return restful.RouteFunction(func(request *restful.Request, response *restful.Response) {
372-
now := time.Now()
371+
return restful.RouteFunction(func(req *restful.Request, response *restful.Response) {
372+
requestReceivedTimestamp, ok := request.ReceivedTimestampFrom(req.Request.Context())
373+
if !ok {
374+
requestReceivedTimestamp = time.Now()
375+
}
373376

374377
delegate := &ResponseWriterDelegator{ResponseWriter: response.ResponseWriter}
375378

@@ -384,16 +387,19 @@ func InstrumentRouteFunc(verb, group, version, resource, subresource, scope, com
384387
}
385388
response.ResponseWriter = rw
386389

387-
routeFunc(request, response)
390+
routeFunc(req, response)
388391

389-
MonitorRequest(request.Request, verb, group, version, resource, subresource, scope, component, deprecated, removedRelease, delegate.Header().Get("Content-Type"), delegate.Status(), delegate.ContentLength(), time.Since(now))
392+
MonitorRequest(req.Request, verb, group, version, resource, subresource, scope, component, deprecated, removedRelease, delegate.Header().Get("Content-Type"), delegate.Status(), delegate.ContentLength(), time.Since(requestReceivedTimestamp))
390393
})
391394
}
392395

393396
// InstrumentHandlerFunc works like Prometheus' InstrumentHandlerFunc but adds some Kubernetes endpoint specific information.
394397
func InstrumentHandlerFunc(verb, group, version, resource, subresource, scope, component string, deprecated bool, removedRelease string, handler http.HandlerFunc) http.HandlerFunc {
395398
return func(w http.ResponseWriter, req *http.Request) {
396-
now := time.Now()
399+
requestReceivedTimestamp, ok := request.ReceivedTimestampFrom(req.Context())
400+
if !ok {
401+
requestReceivedTimestamp = time.Now()
402+
}
397403

398404
delegate := &ResponseWriterDelegator{ResponseWriter: w}
399405

@@ -408,7 +414,7 @@ func InstrumentHandlerFunc(verb, group, version, resource, subresource, scope, c
408414

409415
handler(w, req)
410416

411-
MonitorRequest(req, verb, group, version, resource, subresource, scope, component, deprecated, removedRelease, delegate.Header().Get("Content-Type"), delegate.Status(), delegate.ContentLength(), time.Since(now))
417+
MonitorRequest(req, verb, group, version, resource, subresource, scope, component, deprecated, removedRelease, delegate.Header().Get("Content-Type"), delegate.Status(), delegate.ContentLength(), time.Since(requestReceivedTimestamp))
412418
}
413419
}
414420

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ go_test(
1010
name = "go_default_test",
1111
srcs = [
1212
"context_test.go",
13+
"received_time_test.go",
1314
"requestinfo_test.go",
1415
],
1516
embed = [":go_default_library"],
@@ -25,6 +26,7 @@ go_library(
2526
srcs = [
2627
"context.go",
2728
"doc.go",
29+
"received_time.go",
2830
"requestinfo.go",
2931
],
3032
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/request",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2020 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 request
18+
19+
import (
20+
"context"
21+
"time"
22+
)
23+
24+
type requestReceivedTimestampKeyType int
25+
26+
// requestReceivedTimestampKey is the ReceivedTimestamp (the time the request reached the apiserver)
27+
// key for the context.
28+
const requestReceivedTimestampKey requestReceivedTimestampKeyType = iota
29+
30+
// WithReceivedTimestamp returns a copy of parent context in which the ReceivedTimestamp
31+
// (the time the request reached the apiserver) is set.
32+
//
33+
// If the specified ReceivedTimestamp is zero, no value is set and the parent context is returned as is.
34+
func WithReceivedTimestamp(parent context.Context, receivedTimestamp time.Time) context.Context {
35+
if receivedTimestamp.IsZero() {
36+
return parent
37+
}
38+
return WithValue(parent, requestReceivedTimestampKey, receivedTimestamp)
39+
}
40+
41+
// ReceivedTimestampFrom returns the value of the ReceivedTimestamp key from the specified context.
42+
func ReceivedTimestampFrom(ctx context.Context) (time.Time, bool) {
43+
info, ok := ctx.Value(requestReceivedTimestampKey).(time.Time)
44+
return info, ok
45+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2020 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 request
18+
19+
import (
20+
"context"
21+
"strconv"
22+
"testing"
23+
"time"
24+
)
25+
26+
func TestWithRequestReceiveTime(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
receivedTimestamp time.Time
30+
expected bool
31+
}{
32+
{
33+
name: "request received time is set",
34+
receivedTimestamp: time.Now(),
35+
expected: true,
36+
},
37+
{
38+
name: "request received time is empty",
39+
receivedTimestamp: time.Time{},
40+
expected: false,
41+
},
42+
}
43+
44+
for _, test := range tests {
45+
t.Run(test.name, func(t *testing.T) {
46+
parent := context.TODO()
47+
ctx := WithReceivedTimestamp(parent, test.receivedTimestamp)
48+
if ctx == nil {
49+
t.Fatal("WithReceivedTimestamp: expected a non nil context, got nil")
50+
}
51+
52+
receivedTimestampGot, ok := ReceivedTimestampFrom(ctx)
53+
if test.expected != ok {
54+
t.Errorf("ReceivedTimestampFrom: expected=%s got=%s", strconv.FormatBool(test.expected), strconv.FormatBool(ok))
55+
}
56+
57+
if test.receivedTimestamp != receivedTimestampGot {
58+
t.Errorf("ReceivedTimestampFrom: received timestamp expected=%s but got=%s", test.receivedTimestamp, receivedTimestampGot)
59+
}
60+
})
61+
}
62+
}

staging/src/k8s.io/apiserver/pkg/server/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
690690
handler = genericapifilters.WithAuditAnnotations(handler, c.AuditBackend, c.AuditPolicyChecker)
691691
handler = genericapifilters.WithWarningRecorder(handler)
692692
handler = genericapifilters.WithCacheControl(handler)
693+
handler = genericapifilters.WithRequestReceivedTimestamp(handler)
693694
handler = genericfilters.WithPanicRecovery(handler)
694695
return handler
695696
}

0 commit comments

Comments
 (0)