diff --git a/pkg/collector/corechecks/cluster/ksm/kubernetes_state.go b/pkg/collector/corechecks/cluster/ksm/kubernetes_state.go index d8d31595834d46..01f336083dc12c 100644 --- a/pkg/collector/corechecks/cluster/ksm/kubernetes_state.go +++ b/pkg/collector/corechecks/cluster/ksm/kubernetes_state.go @@ -946,22 +946,41 @@ func (k *KSMCheck) processAnnotationsAsTags() { k.processLabelsOrAnnotationsAsTags("annotation", k.instance.AnnotationsAsTags) } +// parseLabels parses the labels mapper and returns the labels, wildcard template and getAllLabels flag. +func parseLabels(what string, labelsMapper map[string]string) (map[string]string, string, bool) { + labels := make(map[string]string) + var wildcardTemplate string + var getAllLabels bool + + for label, tag := range labelsMapper { + if label == "*" { + wildcardTemplate = tag + getAllLabels = true + continue + } + // KSM converts labels to snake case. + // Ref: https://github.com/kubernetes/kube-state-metrics/blob/v2.2.2/internal/store/utils.go#L133 + label = what + "_" + toSnakeCase(labelRegexp.ReplaceAllString(label, "_")) + labels[label] = tag + } + + return labels, wildcardTemplate, getAllLabels +} + func (k *KSMCheck) processLabelsOrAnnotationsAsTags(what string, configStuffAsTags map[string]map[string]string) { for resourceKind, labelsMapper := range configStuffAsTags { - labels := make(map[string]string) - for label, tag := range labelsMapper { - // KSM converts labels to snake case. - // Ref: https://github.com/kubernetes/kube-state-metrics/blob/v2.2.2/internal/store/utils.go#L133 - label = what + "_" + toSnakeCase(labelRegexp.ReplaceAllString(label, "_")) - labels[label] = tag - } + labels, wildcardTemplate, getAllLabels := parseLabels(what, labelsMapper) if joinsCfg, ok := k.instance.labelJoins["kube_"+resourceKind+"_"+what+"s"]; ok { maps.Copy(joinsCfg.labelsToGet, labels) + joinsCfg.wildcardTemplate = wildcardTemplate + joinsCfg.getAllLabels = getAllLabels } else { joinsConfig := &joinsConfig{ - labelsToMatch: getLabelToMatchForKind(resourceKind), - labelsToGet: labels, + labelsToMatch: getLabelToMatchForKind(resourceKind), + labelsToGet: labels, + wildcardTemplate: wildcardTemplate, + getAllLabels: getAllLabels, } k.instance.labelJoins["kube_"+resourceKind+"_"+what+"s"] = joinsConfig } diff --git a/pkg/collector/corechecks/cluster/ksm/kubernetes_state_label_joins.go b/pkg/collector/corechecks/cluster/ksm/kubernetes_state_label_joins.go index dde9a433e92c25..76555820a5fdcb 100644 --- a/pkg/collector/corechecks/cluster/ksm/kubernetes_state_label_joins.go +++ b/pkg/collector/corechecks/cluster/ksm/kubernetes_state_label_joins.go @@ -9,6 +9,7 @@ package ksm import ( "slices" + "strings" ksmstore "github.com/DataDog/datadog-agent/pkg/kubestatemetrics/store" "github.com/DataDog/datadog-agent/pkg/util/log" @@ -57,9 +58,10 @@ import ( */ type joinsConfig struct { - labelsToMatch []string - labelsToGet map[string]string - getAllLabels bool + labelsToMatch []string + labelsToGet map[string]string + getAllLabels bool + wildcardTemplate string } type labelJoiner struct { @@ -123,6 +125,25 @@ func newLeafNode() *node { } } +// resolveTag resolves the tag key for a given label name and config. +// It substitutes the %%label%% or %%annotation%% placeholder with the label name. +// If the label isn't a "matching label", it will be prefixed by either "label_" or "annotation_", +// if the label is NOT a "matching label", so we can use that to determine which substitution to do. +func resolveTag(labelName string, config *joinsConfig) string { + if strings.HasPrefix(labelName, "label_") { + return strings.ReplaceAll( + config.wildcardTemplate, + "%%label%%", + strings.TrimPrefix(labelName, "label_"), + ) + } + return strings.ReplaceAll( + config.wildcardTemplate, + "%%annotation%%", + strings.TrimPrefix(labelName, "annotation_"), + ) +} + func (lj *labelJoiner) insertMetric(metric ksmstore.DDMetric, config *joinsConfig, tree *node) { current := tree @@ -158,7 +179,14 @@ func (lj *labelJoiner) insertMetric(metric ksmstore.DDMetric, config *joinsConfi for labelName, labelValue := range metric.Labels { isALabelToMatch := slices.Contains(config.labelsToMatch, labelName) if !isALabelToMatch { - current.labelsToAdd = append(current.labelsToAdd, label{labelName, labelValue}) + if ddTagKey, found := config.labelsToGet[labelName]; found { + current.labelsToAdd = append(current.labelsToAdd, label{ddTagKey, labelValue}) + } else if config.wildcardTemplate != "" { + resolvedTagKey := resolveTag(labelName, config) + current.labelsToAdd = append(current.labelsToAdd, label{resolvedTagKey, labelValue}) + } else { + current.labelsToAdd = append(current.labelsToAdd, label{labelName, labelValue}) + } } } } else { diff --git a/pkg/collector/corechecks/cluster/ksm/kubernetes_state_label_joins_test.go b/pkg/collector/corechecks/cluster/ksm/kubernetes_state_label_joins_test.go index 9561742582c6fa..ded5656298130e 100644 --- a/pkg/collector/corechecks/cluster/ksm/kubernetes_state_label_joins_test.go +++ b/pkg/collector/corechecks/cluster/ksm/kubernetes_state_label_joins_test.go @@ -8,6 +8,7 @@ package ksm import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -444,3 +445,42 @@ func Test_labelJoiner(t *testing.T) { }) } } + +func Test_resolveTag(t *testing.T) { + testCases := []struct { + tmpl, label, expected string + }{ + { + "kube_%%label%%", "label_app", "kube_app", + }, + { + "foo_%%label%%_bar", "label_app", "foo_app_bar", + }, + { + "%%label%%%%label%%", "label_app", "appapp", + }, + { + "kube_%%annotation%%", "annotation_app", "kube_app", + }, + { + "foo_%%annotation%%_bar", "annotation_app", "foo_app_bar", + }, + { + "%%annotation%%%%annotation%%", "annotation_app", "appapp", + }, + { + "kube_", "label_app", "kube_", // no template variable + }, + { + "kube_%%foo%%", "label_app", "kube_%%foo%%", // unsupported template variable + }, + } + + for i, testCase := range testCases { + t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { + cfg := &joinsConfig{wildcardTemplate: testCase.tmpl} + tagName := resolveTag(testCase.label, cfg) + assert.Equal(t, testCase.expected, tagName) + }) + } +} diff --git a/pkg/collector/corechecks/cluster/ksm/kubernetes_state_test.go b/pkg/collector/corechecks/cluster/ksm/kubernetes_state_test.go index 193b05d659ccf9..fa9e849e34b675 100644 --- a/pkg/collector/corechecks/cluster/ksm/kubernetes_state_test.go +++ b/pkg/collector/corechecks/cluster/ksm/kubernetes_state_test.go @@ -1389,6 +1389,67 @@ func TestKSMCheck_processLabelsAsTags(t *testing.T) { }, }, }, + { + name: "With wildcard template", + config: &KSMConfig{ + labelJoins: map[string]*joinsConfig{}, + LabelsMapper: map[string]string{}, + LabelsAsTags: map[string]map[string]string{ + "pod": {"*": "%%label%%"}, + }, + }, + expectedJoins: map[string]*joinsConfig{ + "kube_pod_labels": { + labelsToMatch: []string{"pod", "namespace"}, + labelsToGet: map[string]string{}, + getAllLabels: true, + wildcardTemplate: "%%label%%", + }, + }, + }, + { + name: "With wildcard template prefix", + config: &KSMConfig{ + labelJoins: map[string]*joinsConfig{}, + LabelsMapper: map[string]string{}, + LabelsAsTags: map[string]map[string]string{ + "pod": {"*": "prefix_%%label%%", + "special.label/test": "special", + }, + }, + }, + expectedJoins: map[string]*joinsConfig{ + "kube_pod_labels": { + labelsToMatch: []string{"pod", "namespace"}, + labelsToGet: map[string]string{"label_special_label_test": "special"}, + getAllLabels: true, + wildcardTemplate: "prefix_%%label%%", + }, + }, + }, + { + name: "Mixed wildcard and non-wildcard templates", + config: &KSMConfig{ + labelJoins: map[string]*joinsConfig{}, + LabelsMapper: map[string]string{}, + LabelsAsTags: map[string]map[string]string{ + "pod": {"*": "test_%%label%%"}, + "node": {"special.label/test": "special"}, + }, + }, + expectedJoins: map[string]*joinsConfig{ + "kube_pod_labels": { + labelsToMatch: []string{"pod", "namespace"}, + labelsToGet: map[string]string{}, + getAllLabels: true, + wildcardTemplate: "test_%%label%%", + }, + "kube_node_labels": { + labelsToMatch: []string{"node"}, + labelsToGet: map[string]string{"label_special_label_test": "special"}, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1423,6 +1484,24 @@ func TestKSMCheck_processAnnotationsAsTags(t *testing.T) { }, }, }, + { + name: "With wildcard template", + config: &KSMConfig{ + labelJoins: map[string]*joinsConfig{}, + LabelsMapper: map[string]string{}, + AnnotationsAsTags: map[string]map[string]string{ + "pod": {"*": "%%annotation%%"}, + }, + }, + expectedJoins: map[string]*joinsConfig{ + "kube_pod_annotations": { + labelsToMatch: []string{"pod", "namespace"}, + labelsToGet: map[string]string{}, + getAllLabels: true, + wildcardTemplate: "%%annotation%%", + }, + }, + }, } for _, tt := range tests { diff --git a/releasenotes/notes/Support-for-wildcard-tag-collection-in-KSM-38b8509f874a8b48.yaml b/releasenotes/notes/Support-for-wildcard-tag-collection-in-KSM-38b8509f874a8b48.yaml new file mode 100644 index 00000000000000..3362b45a1c18e7 --- /dev/null +++ b/releasenotes/notes/Support-for-wildcard-tag-collection-in-KSM-38b8509f874a8b48.yaml @@ -0,0 +1,11 @@ +# Each section from every release note are combined when the +# CHANGELOG.rst is rendered. So the text needs to be worded so that +# it does not depend on any information only available in another +# section. This may mean repeating some details, but each section +# must be readable independently of the other. +# +# Each section note must be formatted as reStructuredText. +--- +enhancements: + - | + KSM now supports using a wildcard to collect all resource labels/annotations as tags on metrics. \ No newline at end of file