diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go index dcdfc682753..ae0d4d82285 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "strings" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -29,6 +30,9 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/endpoints/responsewriter" "k8s.io/apiserver/pkg/features" v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1" "k8s.io/apiserver/pkg/server/flagz/negotiate" @@ -128,6 +132,31 @@ func (f *flagzCodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { func handleFlagz(componentName string, reg *registry, serializer runtime.NegotiatedSerializer, restrictions negotiate.FlagzEndpointRestrictions) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + requestReceivedTimestamp, ok := request.ReceivedTimestampFrom(r.Context()) + if !ok { + requestReceivedTimestamp = time.Now() + } + delegate := &metrics.ResponseWriterDelegator{ResponseWriter: w} + w = responsewriter.WrapForHTTP1Or2(delegate) + + // Use MonitorRequest instead of InstrumentHandlerFunc because the group, + // version, and deprecated status depend on per-request content negotiation. + // For text/plain requests, group and version remain empty. For structured + // responses (JSON/YAML/CBOR), they are set to the negotiated API group and + // version (e.g., config.k8s.io/v1alpha1). + var group, version string + var deprecated bool + defer func() { + metrics.MonitorRequest(r, "GET", group, version, + "", // resource + DefaultFlagzPath, // subresource + "", // scope + "", // component + deprecated, + "", // removedRelease + delegate.Status(), delegate.ContentLength(), time.Since(requestReceivedTimestamp)) + }() + obj := flagz(componentName, reg.reader) acceptHeader := r.Header.Get("Accept") if strings.TrimSpace(acceptHeader) == "" { @@ -163,8 +192,13 @@ func handleFlagz(componentName string, reg *registry, serializer runtime.Negotia ) return } + // Set group, version, and deprecated from the negotiated target so + // the deferred MonitorRequest records the actual requested API version. targetGV = mediaType.Convert.GroupVersion() - if reg.deprecatedVersions()[targetGV.Version] { + group = targetGV.Group + version = targetGV.Version + deprecated = reg.deprecatedVersions()[targetGV.Version] + if deprecated { w.Header().Set("Warning", `299 - "This version of the flagz endpoint is deprecated. Please use a newer version."`) } writeStructuredResponse(obj, serializer, targetGV, restrictions, w, r) diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go index 075f32b4296..3917deb5403 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go @@ -30,11 +30,14 @@ import ( "k8s.io/apimachinery/pkg/runtime" cbordirect "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/features" v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1" utilfeature "k8s.io/apiserver/pkg/util/feature" cliflag "k8s.io/component-base/cli/flag" featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" "sigs.k8s.io/yaml" ) @@ -377,3 +380,45 @@ func TestNewFlagzCodecFactory(t *testing.T) { t.Fatalf("unknown media type(s) detected - update newFlagzCodecFactory to explicitly handle them: %v", err) } } + +func TestMetrics(t *testing.T) { + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.String("test-flag", "test-value", "usage") + fakeReader := NamedFlagSetsReader{ + FlagSets: cliflag.NamedFlagSets{ + FlagSets: map[string]*pflag.FlagSet{ + "test": fs, + }, + }, + } + + mux := http.NewServeMux() + Install(mux, "test-server", fakeReader) + metrics.Register() + metrics.Reset() + + // text/plain request: group and version should be empty + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultFlagzPath), nil) + if err != nil { + t.Errorf("%v", err) + } + mux.ServeHTTP(httptest.NewRecorder(), req) + + // structured request: group and version should reflect the negotiated version + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultFlagzPath), nil) + if err != nil { + t.Errorf("%v", err) + } + req.Header.Set("Accept", "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz") + mux.ServeHTTP(httptest.NewRecorder(), req) + + expected := strings.NewReader(` + # HELP apiserver_request_total [STABLE] Counter of apiserver requests broken out for each verb, dry run value, group, version, resource, scope, component, and HTTP response code. + # TYPE apiserver_request_total counter + apiserver_request_total{code="200",component="",dry_run="",group="",resource="",scope="",subresource="/flagz",verb="GET",version=""} 1 + apiserver_request_total{code="200",component="",dry_run="",group="config.k8s.io",resource="",scope="",subresource="/flagz",verb="GET",version="v1alpha1"} 1 +`) + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, expected, "apiserver_request_total"); err != nil { + t.Error(err) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go index aed4bf6a20a..a234f828ce0 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go +++ b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go @@ -31,6 +31,9 @@ import ( "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/endpoints/responsewriter" "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server/statusz/negotiate" @@ -153,6 +156,31 @@ func (f *statuszCodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { func handleStatusz(componentName string, reg statuszRegistry, serializer runtime.NegotiatedSerializer, restrictions negotiate.StatuszEndpointRestrictions) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + requestReceivedTimestamp, ok := request.ReceivedTimestampFrom(r.Context()) + if !ok { + requestReceivedTimestamp = time.Now() + } + delegate := &metrics.ResponseWriterDelegator{ResponseWriter: w} + w = responsewriter.WrapForHTTP1Or2(delegate) + + // Use MonitorRequest instead of InstrumentHandlerFunc because the group, + // version, and deprecated status depend on per-request content negotiation. + // For text/plain requests, group and version remain empty. For structured + // responses (JSON/YAML/CBOR), they are set to the negotiated API group and + // version (e.g., config.k8s.io/v1alpha1). + var group, version string + var deprecated bool + defer func() { + metrics.MonitorRequest(r, "GET", group, version, + "", // resource + DefaultStatuszPath, // subresource + "", // scope + "", // component + deprecated, + "", // removedRelease + delegate.Status(), delegate.ContentLength(), time.Since(requestReceivedTimestamp)) + }() + obj := statusz(componentName, reg) acceptHeader := r.Header.Get("Accept") if strings.TrimSpace(acceptHeader) == "" { @@ -188,8 +216,12 @@ func handleStatusz(componentName string, reg statuszRegistry, serializer runtime ) return } + // Set group, version, and deprecated from the negotiated target so + // the deferred MonitorRequest records the actual requested API version. targetGV = mediaType.Convert.GroupVersion() - deprecated := reg.deprecatedVersions()[targetGV.Version] + group = targetGV.Group + version = targetGV.Version + deprecated = reg.deprecatedVersions()[targetGV.Version] if deprecated { w.Header().Set("Warning", `299 - "This version of the statusz endpoint is deprecated. Please use a newer version."`) } diff --git a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go index 9b0b5515e2b..90043f7ac65 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go @@ -32,9 +32,12 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1" @@ -460,3 +463,48 @@ func TestNewStatuszCodecFactory(t *testing.T) { t.Fatalf("unknown media type(s) detected - update newStatuszCodecFactory to explicitly handle them: %v", err) } } + +func TestMetrics(t *testing.T) { + fakeStartTime := time.Now() + fakeBinaryVersion := parseVersion(t, "1.31") + fakeEmulationVersion := parseVersion(t, "1.30") + fakeListedPaths := []string{"/livez/ping", "/readyz/ping"} + + reg := fakeRegistry{ + startTime: fakeStartTime, + goVer: "1.21", + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + } + + mux := http.NewServeMux() + Install(mux, "test-server", reg) + metrics.Register() + metrics.Reset() + + // text/plain request: group and version should be empty + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultStatuszPath), nil) + if err != nil { + t.Errorf("%v", err) + } + mux.ServeHTTP(httptest.NewRecorder(), req) + + // structured request: group and version should reflect the negotiated version + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultStatuszPath), nil) + if err != nil { + t.Errorf("%v", err) + } + req.Header.Set("Accept", "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz") + mux.ServeHTTP(httptest.NewRecorder(), req) + + expected := strings.NewReader(` + # HELP apiserver_request_total [STABLE] Counter of apiserver requests broken out for each verb, dry run value, group, version, resource, scope, component, and HTTP response code. + # TYPE apiserver_request_total counter + apiserver_request_total{code="200",component="",dry_run="",group="",resource="",scope="",subresource="/statusz",verb="GET",version=""} 1 + apiserver_request_total{code="200",component="",dry_run="",group="config.k8s.io",resource="",scope="",subresource="/statusz",verb="GET",version="v1alpha1"} 1 +`) + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, expected, "apiserver_request_total"); err != nil { + t.Error(err) + } +}