Merge pull request #137021 from yongruilin/master_zpages-metrics

Instrument flagz and statusz endpoints with apiserver request metrics
This commit is contained in:
Kubernetes Prow Robot
2026-02-28 07:57:30 +05:30
committed by GitHub
4 changed files with 161 additions and 2 deletions

View File

@@ -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,
"flagz", // resource
"", // subresource
"", // scope
componentName, // 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)

View File

@@ -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="test-server",dry_run="",group="",resource="flagz",scope="",subresource="",verb="GET",version=""} 1
apiserver_request_total{code="200",component="test-server",dry_run="",group="config.k8s.io",resource="flagz",scope="",subresource="",verb="GET",version="v1alpha1"} 1
`)
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, expected, "apiserver_request_total"); err != nil {
t.Error(err)
}
}

View File

@@ -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,
"statusz", // resource
"", // subresource
"", // scope
componentName, // 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."`)
}

View File

@@ -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="test-server",dry_run="",group="",resource="statusz",scope="",subresource="",verb="GET",version=""} 1
apiserver_request_total{code="200",component="test-server",dry_run="",group="config.k8s.io",resource="statusz",scope="",subresource="",verb="GET",version="v1alpha1"} 1
`)
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, expected, "apiserver_request_total"); err != nil {
t.Error(err)
}
}