Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draft: add `inject-per-series-metadata' #2632

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion internal/store/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type Builder struct {
buildCustomResourceStoresFunc ksmtypes.BuildCustomResourceStoresFunc
allowAnnotationsList map[string][]string
allowLabelsList map[string][]string
injectPerSeriesMetadata bool
utilOptions *options.Options
// namespaceFilter is inside fieldSelectorFilter
fieldSelectorFilter string
Expand Down Expand Up @@ -245,6 +246,13 @@ func (b *Builder) allowList(list map[string][]string) (map[string][]string, erro
return m, nil
}

// WithAllowAnnotations configures which annotations can be returned for metrics
func (b *Builder) WithInjectPerSeriesMetadata(inject bool) error {
var err error
b.injectPerSeriesMetadata = inject
return err
}

// WithAllowAnnotations configures which annotations can be returned for metrics
func (b *Builder) WithAllowAnnotations(annotations map[string][]string) error {
var err error
Expand Down Expand Up @@ -463,7 +471,7 @@ func (b *Builder) buildStorageClassStores() []cache.Store {
}

func (b *Builder) buildPodStores() []cache.Store {
return b.buildStoresFunc(podMetricFamilies(b.allowAnnotationsList["pods"], b.allowLabelsList["pods"]), &v1.Pod{}, createPodListWatch, b.useAPIServerCache)
return b.buildStoresFunc(podMetricFamilies(b.injectPerSeriesMetadata, b.allowAnnotationsList["pods"], b.allowLabelsList["pods"]), &v1.Pod{}, createPodListWatch, b.useAPIServerCache)
}

func (b *Builder) buildCsrStores() []cache.Store {
Expand Down
22 changes: 13 additions & 9 deletions internal/store/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ var (
podStatusReasons = []string{"Evicted", "NodeAffinity", "NodeLost", "Shutdown", "UnexpectedAdmissionError"}
)

func podMetricFamilies(allowAnnotationsList, allowLabelsList []string) []generator.FamilyGenerator {
func podMetricFamilies(injectPerSeriesMetadata bool, allowAnnotationsList []string, allowLabelsList []string) []generator.FamilyGenerator {
mc := &MetricConfig{
Copy link
Contributor Author

@jacobstr jacobstr Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this so instead of adding 3 arguments to each generator, I add one. I suppose it makes it less "pure" as the behavior of the function is dictated by a complex configuration object.

InjectPerSeriesMetadata: injectPerSeriesMetadata,
AllowAnnotations: allowAnnotationsList,
AllowLabels: allowLabelsList,
}
return []generator.FamilyGenerator{
createPodCompletionTimeFamilyGenerator(),
createPodContainerInfoFamilyGenerator(),
Expand Down Expand Up @@ -82,7 +87,7 @@ func podMetricFamilies(allowAnnotationsList, allowLabelsList []string) []generat
createPodSpecVolumesPersistentVolumeClaimsInfoFamilyGenerator(),
createPodSpecVolumesPersistentVolumeClaimsReadonlyFamilyGenerator(),
createPodStartTimeFamilyGenerator(),
createPodStatusPhaseFamilyGenerator(),
createPodStatusPhaseFamilyGenerator(mc),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The full implementation would add this param to every metric family generator. This is also the reason I'm bundling up the configuration in a struct and passing it as a single unit.

createPodStatusQosClassFamilyGenerator(),
createPodStatusReadyFamilyGenerator(),
createPodStatusReadyTimeFamilyGenerator(),
Expand Down Expand Up @@ -1333,7 +1338,7 @@ func createPodStartTimeFamilyGenerator() generator.FamilyGenerator {
)
}

func createPodStatusPhaseFamilyGenerator() generator.FamilyGenerator {
func createPodStatusPhaseFamilyGenerator(mc *MetricConfig) generator.FamilyGenerator {
return *generator.NewFamilyGeneratorWithStability(
"kube_pod_status_phase",
"The pods current phase.",
Expand Down Expand Up @@ -1361,13 +1366,12 @@ func createPodStatusPhaseFamilyGenerator() generator.FamilyGenerator {

ms := make([]*metric.Metric, len(phases))

for i, p := range phases {
ms[i] = &metric.Metric{

for i, ph := range phases {
ms[i] = injectLabelsAndAnnos(&metric.Metric{
LabelKeys: []string{"phase"},
LabelValues: []string{p.n},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were shadowing the outer p (Pod).

Value: boolFloat64(p.v),
}
LabelValues: []string{ph.n},
Value: boolFloat64(ph.v),
}, mc, &p.ObjectMeta)
}

return &metric.Family{
Expand Down
6 changes: 3 additions & 3 deletions internal/store/pod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2181,8 +2181,8 @@ func TestPodStore(t *testing.T) {
}

for i, c := range cases {
c.Func = generator.ComposeMetricGenFuncs(podMetricFamilies(c.AllowAnnotationsList, c.AllowLabelsList))
c.Headers = generator.ExtractMetricFamilyHeaders(podMetricFamilies(c.AllowAnnotationsList, c.AllowLabelsList))
c.Func = generator.ComposeMetricGenFuncs(podMetricFamilies(false, c.AllowAnnotationsList, c.AllowLabelsList))
c.Headers = generator.ExtractMetricFamilyHeaders(podMetricFamilies(false, c.AllowAnnotationsList, c.AllowLabelsList))
if err := c.run(); err != nil {
t.Errorf("unexpected collecting result in %vth run:\n%s", i, err)
}
Expand All @@ -2192,7 +2192,7 @@ func TestPodStore(t *testing.T) {
func BenchmarkPodStore(b *testing.B) {
b.ReportAllocs()

f := generator.ComposeMetricGenFuncs(podMetricFamilies(nil, nil))
f := generator.ComposeMetricGenFuncs(podMetricFamilies(false, nil, nil))

pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Expand Down
18 changes: 18 additions & 0 deletions internal/store/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/api/resource"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation"

"k8s.io/kube-state-metrics/v2/pkg/metric"
Expand All @@ -38,6 +39,12 @@ var (
conditionStatuses = []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionUnknown}
)

type MetricConfig struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be made private since it's built / used internally to this module.

InjectPerSeriesMetadata bool
AllowAnnotations []string
AllowLabels []string
}

func resourceVersionMetric(rv string) []*metric.Metric {
v, err := strconv.ParseFloat(rv, 64)
if err != nil {
Expand Down Expand Up @@ -175,6 +182,17 @@ func isPrefixedNativeResource(name v1.ResourceName) bool {
return strings.Contains(string(name), v1.ResourceDefaultNamespacePrefix)
}

// convenience wrapper to inject allow-listed labels and annotations to a metric if per-series injection is enabled.
func injectLabelsAndAnnos(m *metric.Metric, metricConfig *MetricConfig, obj *metav1.ObjectMeta) *metric.Metric {
if !metricConfig.InjectPerSeriesMetadata {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with the pass-by-reference + early guard clause, this should be "0-cost" for those leaving the --inject-per-series-metadata at it's default of false.

return m
}
labelKeys, labelValues := createPrometheusLabelKeysValues("label", obj.Labels, metricConfig.AllowLabels)
annotationKeys, annotationValues := createPrometheusLabelKeysValues("annotation", obj.Annotations, metricConfig.AllowAnnotations)
m.LabelKeys, m.LabelValues = mergeKeyValues(m.LabelKeys, m.LabelValues, annotationKeys, annotationValues, labelKeys, labelValues)
return m
}

// createPrometheusLabelKeysValues takes in passed kubernetes annotations/labels
// and associated allowed list in kubernetes label format.
// It returns only those allowed annotations/labels that exist in the list and converts them to Prometheus labels.
Expand Down
3 changes: 3 additions & 0 deletions pkg/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ func RunKubeStateMetrics(ctx context.Context, opts *options.Options) error {
if err := storeBuilder.WithAllowLabels(opts.LabelsAllowList); err != nil {
return fmt.Errorf("failed to set up labels allowlist: %v", err)
}
if err := storeBuilder.WithInjectPerSeriesMetadata(opts.InjectPerSeriesMetadata); err != nil {
return fmt.Errorf("failed to configure per series metadata injection: %v", err)
}

ksmMetricsRegistry.MustRegister(
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
Expand Down
3 changes: 3 additions & 0 deletions pkg/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ type Options struct {
MetricOptInList MetricSet `yaml:"metric_opt_in_list"`
Resources ResourceSet `yaml:"resources"`

InjectPerSeriesMetadata bool `yaml:"inject_per_series_metadata"`

cmd *cobra.Command
Apiserver string `yaml:"apiserver"`
CustomResourceConfig string `yaml:"custom_resource_config"`
Expand Down Expand Up @@ -164,6 +166,7 @@ func (o *Options) AddFlags(cmd *cobra.Command) {
o.cmd.Flags().Var(&o.LabelsAllowList, "metric-labels-allowlist", "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the labels metrics are not exposed. To include them, provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (Example: '=namespaces=[k8s-label-1,k8s-label-n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (Example: '=pods=[*]'). Additionally, an asterisk (*) can be provided as a key, which will resolve to all resources, i.e., assuming '--resources=deployments,pods', '=*=[*]' will resolve to '=deployments=[*],pods=[*]'.")
o.cmd.Flags().Var(&o.MetricAllowlist, "metric-allowlist", "Comma-separated list of metrics to be exposed. This list comprises of exact metric names and/or *ECMAScript-based* regex patterns. The allowlist and denylist are mutually exclusive.")
o.cmd.Flags().Var(&o.MetricDenylist, "metric-denylist", "Comma-separated list of metrics not to be enabled. This list comprises of exact metric names and/or *ECMAScript-based* regex patterns. The allowlist and denylist are mutually exclusive.")
o.cmd.Flags().BoolVar(&o.InjectPerSeriesMetadata, "inject-per-series-metadata", false, "Propagate labels and annotations from object metadata to all respective metrics rather than simply populating kube_<resource>_labels and kube_<resource>_annotations. Honors metric-annotations-allowlist and metric-labels-allowlist.")
o.cmd.Flags().Var(&o.MetricOptInList, "metric-opt-in-list", "Comma-separated list of metrics which are opt-in and not enabled by default. This is in addition to the metric allow- and denylists")
o.cmd.Flags().Var(&o.Namespaces, "namespaces", fmt.Sprintf("Comma-separated list of namespaces to be enabled. Defaults to %q", &DefaultNamespaces))
o.cmd.Flags().Var(&o.NamespacesDenylist, "namespaces-denylist", "Comma-separated list of namespaces not to be enabled. If namespaces and namespaces-denylist are both set, only namespaces that are excluded in namespaces-denylist will be used.")
Expand Down
Loading