Skip to content

Cluster scope metrics is exposed duplicately when using --namespaces cli option #2878

@hanaldo1

Description

@hanaldo1

What happened:
Cluster scoped metric like kube_node_created is duplicated as many times as the number of namespace when using --namespaces=...

So, warning log like below is shown in the Prometheus.

ts=2026-02-26T05:56:38.676Z caller=scrape.go:1754 level=warn component="scrape manager" scrape_pool=serviceMonitor/<namespace>/kube-state-metrics/0 target=http://<pod IP>:8080/metrics msg="Error on ingesting samples with different value but same timestamp" num_dropped=7440

What you expected to happen:
Cluster scoped metric isn't duplicated as many times as the number of namespace.

How to reproduce it (as minimally and precisely as possible):

  1. deploy kube-state-metrics with --namespaces cli options

    a. kube-prometheus-stack Helm chart values

    kube-state-metrics:
    ....
      namespaces:
        - namespaceA
        - namespaceB
        - namespaceC

    => pass 3 namespace

  2. check metrics via kube-state-metrics' /metrics API

    # HELP kube_namespace_created [STABLE] Unix creation timestamp
    # TYPE kube_namespace_created gauge
    kube_namespace_created{namespace="namespaceA"} 1.715672771e+09
    kube_namespace_created{namespace="namespaceA"} 1.715672771e+09
    kube_namespace_created{namespace="namespaceA"} 1.715672771e+09
    
    # HELP kube_namespace_status_phase [STABLE] kubernetes namespace status phase.
    # TYPE kube_namespace_status_phase gauge
    ... (same above)
    

    => kube_namespace_created metric is repeated as many times as the number of namespace (for this example, repeat 3 times)

Anything else we need to know?:
Only cluster scope metric is duplicated.

a. kube-prometheus-stack Helm chart values

kube-state-metrics:
....
  collectors:
    - configmaps
    - cronjobs
    - daemonsets
    - deployments
    - endpointslices
    - ingresses
    - jobs
    - namespaces
    - nodes
    - persistentvolumeclaims
    - persistentvolumes
    - poddisruptionbudgets
    - pods
    - replicasets
    - replicationcontrollers
    - secrets
    - services
    - statefulsets
    - storageclasses
    - volumeattachments

b. duplicated metric list

kube_namespace_created
kube_namespace_status_phase
kube_node_created
kube_node_info
kube_node_labels
kube_node_spec_unschedulable
kube_node_status_allocatable
kube_node_status_capacity
kube_node_status_addresses
kube_persistentvolume_claim_ref
kube_persistentvolume_status_phase
kube_persistentvolume_info
kube_persistentvolume_capacity_bytes
kube_persistentvolume_created
kube_persistentvolume_volume_mode
kube_storageclass_info
kube_storageclass_created
kube_volumeattachment_labels
kube_volumeattachment_info
kube_volumeattachment_created
kube_volumeattachment_spec_source_persistentvolume
kube_volumeattachment_status_attached
kube_volumeattachment_status_attachment_metadata

=> I checked this from prometheus log like below after setting prometheus' log level as debug

ts=2026-02-26T06:16:38.660Z caller=scrape.go:1793 level=debug component="scrape manager" scrape_pool=serviceMonitor/<namespace>/kube-state-metrics/0 target=http://<pod IP>:8080/metrics msg="Duplicate sample for timestamp" series="kube_namespace_created{namespace=\"namespaceA\"}"
...
ts=2026-02-26T06:16:38.754Z caller=scrape.go:1754 level=warn component="scrape manager" scrape_pool=serviceMonitor/<namespace>/kube-state-metrics/0 target=http://<pod IP>:8080/metrics msg="Error on ingesting samples with different value but same timestamp" num_dropped=7440

So I'm not sure if it's right, but I think, below logic is reason of duplicated metric for cluster scope.

https://github.com/kubernetes/kube-state-metrics/blob/v2.13.0/internal/store/builder.go

// WithGenerateStoresFunc configures a custom generate store function
func (b *Builder) WithGenerateStoresFunc(f ksmtypes.BuildStoresFunc) {
	b.buildStoresFunc = f
}

// DefaultGenerateStoresFunc returns default buildStores function
func (b *Builder) DefaultGenerateStoresFunc() ksmtypes.BuildStoresFunc {
	return b.buildStores
}

...

func (b *Builder) buildNamespaceStores() []cache.Store {
	return b.buildStoresFunc(namespaceMetricFamilies(b.allowAnnotationsList["namespaces"], b.allowLabelsList["namespaces"]), &v1.Namespace{}, createNamespaceListWatch, b.useAPIServerCache)
}

...

func (b *Builder) buildStores(...) []cache.Store {
	...

	if b.namespaces.IsAllNamespaces() {
		store := metricsstore.NewMetricsStore(
			familyHeaders,
			composedMetricGenFuncs,
		)
		if b.fieldSelectorFilter != "" {
			klog.InfoS("FieldSelector is used", "fieldSelector", b.fieldSelectorFilter)
		}
		listWatcher := listWatchFunc(b.kubeClient, v1.NamespaceAll, b.fieldSelectorFilter)
		b.startReflector(expectedType, store, listWatcher, useAPIServerCache)
		return []cache.Store{store}
	}

	stores := make([]cache.Store, 0, len(b.namespaces))
	for _, ns := range b.namespaces {
		store := metricsstore.NewMetricsStore(
			familyHeaders,
			composedMetricGenFuncs,
		)
		if b.fieldSelectorFilter != "" {
			klog.InfoS("FieldSelector is used", "fieldSelector", b.fieldSelectorFilter)
		}
		listWatcher := listWatchFunc(b.kubeClient, ns, b.fieldSelectorFilter)
		b.startReflector(expectedType, store, listWatcher, useAPIServerCache)
		stores = append(stores, store)
	}

	return stores
}

builder.buildStoresFunc is set as buildStores function.

so when building namespace store using buildNamespaceStores(), namespace store is created as many times as the number of namespace, because --namespaces options is used not all namespace.

I solved it for now by not passing namespaces list.
But I want to use namespaces options to exclude namespace that isn't need to scrape for metric.

So, I hope this problem is solved.

Environment:

  • kube-state-metrics version: 2.13.0
  • Kubernetes version (use kubectl version): 1.34
  • Cloud provider or hardware configuration:
  • Other info:
    • kube-prometheus-stack version: 65.0.0
    • prometheus: 2.54.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugCategorizes issue or PR as related to a bug.needs-triageIndicates an issue or PR lacks a `triage/foo` label and requires one.

    Type

    No type

    Projects

    Status

    Needs Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions