Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
37 changes: 28 additions & 9 deletions pkg/collector/corechecks/cluster/ksm/kubernetes_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package ksm

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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)
})
}
}
79 changes: 79 additions & 0 deletions pkg/collector/corechecks/cluster/ksm/kubernetes_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 collection all resource labels/annotations as tags on metrics.