mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-17 11:28:58 +00:00
Merge pull request #137021 from yongruilin/master_zpages-metrics
Instrument flagz and statusz endpoints with apiserver request metrics
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."`)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user