From 1d2651da7de7aa840d32fc7195e44e18e869e234 Mon Sep 17 00:00:00 2001 From: Richa Banker Date: Fri, 8 May 2026 13:37:32 -0700 Subject: [PATCH] defer metric registration to runtime entry point via constructors Signed-off-by: Richa Banker Kubernetes-commit: 8721c830cf013a92a805b46eb163736f97aba2aa --- plugin/pkg/client/auth/exec/exec.go | 1 + rest/client.go | 3 +++ rest/config.go | 3 +++ rest/transport.go | 2 ++ tools/metrics/metrics.go | 41 ++++++++++++++++++++++++++++- transport/transport.go | 2 ++ 6 files changed, 51 insertions(+), 1 deletion(-) diff --git a/plugin/pkg/client/auth/exec/exec.go b/plugin/pkg/client/auth/exec/exec.go index b2393f4dd..60f9da50d 100644 --- a/plugin/pkg/client/auth/exec/exec.go +++ b/plugin/pkg/client/auth/exec/exec.go @@ -159,6 +159,7 @@ func (s *sometimes) Do(f func()) { // GetAuthenticator returns an exec-based plugin for providing client credentials. func GetAuthenticator(config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) { + metrics.EnsureRegistered() return newAuthenticator(globalCache, term.IsTerminal, config, cluster) } diff --git a/rest/client.go b/rest/client.go index a085c334f..d92c1b647 100644 --- a/rest/client.go +++ b/rest/client.go @@ -32,6 +32,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" clientfeatures "k8s.io/client-go/features" + "k8s.io/client-go/tools/metrics" "k8s.io/client-go/util/flowcontrol" ) @@ -110,6 +111,8 @@ type RESTClient struct { // NewRESTClient creates a new RESTClient. This client performs generic REST functions // such as Get, Put, Post, and Delete on specified paths. func NewRESTClient(baseURL *url.URL, versionedAPIPath string, config ClientContentConfig, rateLimiter flowcontrol.RateLimiter, client *http.Client) (*RESTClient, error) { + metrics.EnsureRegistered() + base := *baseURL if !strings.HasSuffix(base.Path, "/") { base.Path += "/" diff --git a/rest/config.go b/rest/config.go index 82d4f7136..3d4fb720c 100644 --- a/rest/config.go +++ b/rest/config.go @@ -37,6 +37,7 @@ import ( "k8s.io/client-go/features" "k8s.io/client-go/pkg/version" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/tools/metrics" "k8s.io/client-go/transport" certutil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/flowcontrol" @@ -355,6 +356,8 @@ func RESTClientFor(config *Config) (*RESTClient, error) { // Note that the http client takes precedence over the transport values configured. // The http client defaults to the `http.DefaultClient` if nil. func RESTClientForConfigAndClient(config *Config, httpClient *http.Client) (*RESTClient, error) { + metrics.EnsureRegistered() + if config.GroupVersion == nil { return nil, fmt.Errorf("GroupVersion is required when initializing a RESTClient") } diff --git a/rest/transport.go b/rest/transport.go index 53f986cbf..c95824040 100644 --- a/rest/transport.go +++ b/rest/transport.go @@ -23,6 +23,7 @@ import ( "k8s.io/client-go/pkg/apis/clientauthentication" "k8s.io/client-go/plugin/pkg/client/auth/exec" + "k8s.io/client-go/tools/metrics" "k8s.io/client-go/transport" ) @@ -82,6 +83,7 @@ func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTrip // TransportConfig converts a client config to an appropriate transport config. func (c *Config) TransportConfig() (*transport.Config, error) { + metrics.EnsureRegistered() conf := &transport.Config{ UserAgent: c.UserAgent, Transport: c.Transport, diff --git a/tools/metrics/metrics.go b/tools/metrics/metrics.go index 2f626475b..70e3b58e9 100644 --- a/tools/metrics/metrics.go +++ b/tools/metrics/metrics.go @@ -25,7 +25,37 @@ import ( "time" ) -var registerMetrics sync.Once +var ( + registerMetrics sync.Once + ensureRegisteredOnce sync.Once + // ensureRegisteredFn, if set via RegisterOpts.RegisterFn, is invoked + // exactly once before any rest client is constructed. Adapter packages + // (e.g. k8s.io/component-base/metrics/prometheus/restclient) install + // this callback in their init() so that the actual registration with + // legacyregistry — and the metric Create() that reads + // feature-gate-derived options like NativeHistograms — happens at + // runtime rather than at init() time. See EnsureRegistered for the + // caller-side contract. + ensureRegisteredFn func() +) + +// EnsureRegistered invokes the callback installed via RegisterOpts.RegisterFn (if +// any) exactly once. Callers should treat it as idempotent; subsequent calls are +// effectively free. +// +// New public constructors or entry points for packages in client-go that create +// a REST client, HTTP transport, or credential provider should invoke EnsureRegistered() +// at the very beginning of the function. Adapter Observe methods +// also call EnsureRegistered(), so no observations are lost if a constructor +// forgets to invoke it, but if invoked, the entrypoint call shifts registration from +// "first-observation" to "first client construction", meaning the metric +// series is visible to Promteheus scrapes from process startup rather than +// appearing only after the first request. +func EnsureRegistered() { + if ensureRegisteredFn != nil { + ensureRegisteredOnce.Do(ensureRegisteredFn) + } +} // DurationMetric is a measurement of some amount of time. type DurationMetric interface { @@ -163,6 +193,12 @@ type RegisterOpts struct { TransportCAReloads TransportCAReloadsMetric TransportCertRotationGCCalls TransportCertRotationGCCallsMetric TransportCacheGCCalls TransportCacheGCCallsMetric + + // RegisterFn, if non-nil, is invoked exactly once by EnsureRegistered(). + // before the first rest client is constructed. Adapters use this to defer + // registrations that depend on runtime state (eg., feature gates read my metric + // Create() without changing the import side contract of the adapter package.) + RegisterFn func() } // Register registers metrics for the rest client to use. This can @@ -217,6 +253,9 @@ func Register(opts RegisterOpts) { if opts.TransportCacheGCCalls != nil { TransportCacheGCCalls = opts.TransportCacheGCCalls } + if opts.RegisterFn != nil { + ensureRegisteredFn = opts.RegisterFn + } }) } diff --git a/transport/transport.go b/transport/transport.go index be97b7621..c4595da30 100644 --- a/transport/transport.go +++ b/transport/transport.go @@ -29,12 +29,14 @@ import ( utilnet "k8s.io/apimachinery/pkg/util/net" clientgofeaturegate "k8s.io/client-go/features" + "k8s.io/client-go/tools/metrics" "k8s.io/klog/v2" ) // New returns an http.RoundTripper that will provide the authentication // or transport level security defined by the provided Config. func New(config *Config) (http.RoundTripper, error) { + metrics.EnsureRegistered() // Set transport level security if config.Transport != nil && (config.HasCA() || config.HasCertAuth() || config.HasCertCallback() || config.TLS.Insecure) { return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed")