diff --git a/docs/customresourcestate-metrics.md b/docs/customresourcestate-metrics.md index 7d2052a75c..12dc8fb7ee 100644 --- a/docs/customresourcestate-metrics.md +++ b/docs/customresourcestate-metrics.md @@ -552,8 +552,11 @@ path: [status, namespaces, "*", status] labelsFromPath: # this can be combined with the wildcard prefixes feature introduced in #2052 "available_*": [available] + "lorem_*": [pending] +# this can be combined with dynamic valueFrom expressions introduced in #2068 +valueFrom: [pending_resourceB] # outputs: -...{...,available_resourceA="10",available_resourceB="20",...} ... +...{...,available_resourceA="10",available_resourceB="20",lorem_resourceA="0",lorem_resourceB="6"...} 6 ``` ### Wildcard matching of version and kind fields diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index b79c1adda3..553cd1ae60 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -228,6 +228,26 @@ type compiledGauge struct { labelFromKey string } +func underscoresToIndices(extractedValueFrom string, it interface{}) interface{} { + // `it` is the search space. + _, isResolvable := it.(map[string]interface{}) + if !isResolvable { + return nil + } + // `extractedValueFrom` is the search term. + // Split `extractedValueFrom` by underscores. + terms := strings.Split(extractedValueFrom, "_") + resolvedTerm := interface{}(terms[0]) + for _, term := range terms[1:] { + t, ok := it.(map[string]interface{})[term] + if !ok { + return resolvedTerm + } + resolvedTerm = t + } + return resolvedTerm +} + func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error) { onError := func(err error) { errs = append(errs, fmt.Errorf("%s: %v", c.Path(), err)) @@ -250,8 +270,17 @@ func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error) // "[...]" and not "[]". len(sValueFrom) > 2 { extractedValueFrom := sValueFrom[1 : len(sValueFrom)-1] - if key == extractedValueFrom { - gotFloat, err := toFloat64(it, c.NilIsZero) + if strings.HasPrefix(extractedValueFrom, key) { + var gotFloat float64 + var err error + if strings.Contains(extractedValueFrom, "_") { + resolvedExtractedValueFrom := underscoresToIndices(extractedValueFrom, it) + if _, didResolveFullPath := resolvedExtractedValueFrom.(string); didResolveFullPath { + gotFloat, err = toFloat64(resolvedExtractedValueFrom, c.NilIsZero) + } + } else { + gotFloat, err = toFloat64(it, c.NilIsZero) + } if err != nil { onError(fmt.Errorf("[%s]: %w", key, err)) continue @@ -710,23 +739,24 @@ func famGen(f compiledFamily) generator.FamilyGenerator { } } +func findWildcard(path valuePath, i *int) bool { + for ; *i < len(path); *i++ { + if path[*i].part == "*" { + return true + } + } + return false +} + func resolveWildcard(path valuePath, object map[string]interface{}) []valuePath { if path == nil { return nil } - fn := func(i *int) bool { - for ; *i < len(path); *i++ { - if path[*i].part == "*" { - return true - } - } - return false - } checkpoint := object var expandedPaths []valuePath var list []interface{} var l int - for i, j := 0, 0; fn(&i); /* i is at "*" now */ { + for i, j := 0, 0; findWildcard(path, &i); /* i is at "*" now */ { for ; j < i; j++ { maybeCheckpoint, ok := checkpoint[path[j].part] if !ok { @@ -764,23 +794,10 @@ func generate(u *unstructured.Unstructured, f compiledFamily, errLog klog.Verbos klog.V(10).InfoS("Checked", "compiledFamilyName", f.Name, "unstructuredName", u.GetName()) var metrics []*metric.Metric baseLabels := f.BaseLabels(u.Object) - fn := func() { - values, errorSet := scrapeValuesFor(f.Each, u.Object) - for _, err := range errorSet { - errLog.ErrorS(err, f.Name) - } - - for _, v := range values { - v.DefaultLabels(baseLabels) - metrics = append(metrics, v.ToMetric()) - } - klog.V(10).InfoS("Produced metrics for", "compiledFamilyName", f.Name, "metricsLength", len(metrics), "unstructuredName", u.GetName()) - } if f.Each.Path() != nil { fPaths := resolveWildcard(f.Each.Path(), u.Object) for _, fPath := range fPaths { f.Each.SetPath(fPath) - fn() } } @@ -797,9 +814,19 @@ func generate(u *unstructured.Unstructured, f compiledFamily, errLog klog.Verbos if len(labelsFromPath) > 0 { f.Each.SetLabelFromPath(labelsFromPath) } - fn() } + values, errorSet := scrapeValuesFor(f.Each, u.Object) + for _, err := range errorSet { + errLog.ErrorS(err, f.Name) + } + + for _, v := range values { + v.DefaultLabels(baseLabels) + metrics = append(metrics, v.ToMetric()) + } + klog.V(10).InfoS("Produced metrics for", "compiledFamilyName", f.Name, "metricsLength", len(metrics), "unstructuredName", u.GetName()) + return &metric.Family{ Metrics: metrics, } diff --git a/pkg/customresourcestate/registry_factory_test.go b/pkg/customresourcestate/registry_factory_test.go index 25f3621aa1..f3a240480e 100644 --- a/pkg/customresourcestate/registry_factory_test.go +++ b/pkg/customresourcestate/registry_factory_test.go @@ -102,7 +102,8 @@ func init() { "metadata": Obj{ "name": "foo", "labels": Obj{ - "foo": "bar", + "foo": "bar", + "numStr": "42", }, "annotations": Obj{ "qux": "quxx", @@ -371,6 +372,17 @@ func Test_values(t *testing.T) { }, wantResult: []eachValue{ newEachValue(t, 1, "bar", "baz"), }}, + {name: "dynamic valueFrom", each: &compiledGauge{ + compiledCommon: compiledCommon{ + path: mustCompilePath(t, "metadata"), + labelFromPath: map[string]valuePath{ + "lorem_*": mustCompilePath(t, "labels"), + }, + }, + ValueFrom: mustCompilePath(t, "labels_numStr"), + }, wantResult: []eachValue{ + newEachValue(t, 42, "lorem_numStr", "42", "lorem_foo", "bar"), + }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {