From 0f91e626f38aa392a7efbae456e461c9472d936d Mon Sep 17 00:00:00 2001 From: Pranshu Srivastava Date: Tue, 16 May 2023 04:58:29 +0530 Subject: [PATCH] Support list expansions Allow CRS configuration to take in path values for lists, denoted by "*". This means it's possible to specify `[..., , "*", foo, ...]` in the configuration to dynamically generate multiple metrics that reflect different states of `foo` in various list elements. --- docs/customresourcestate-metrics.md | 23 ++++ pkg/customresourcestate/registry_factory.go | 125 ++++++++++++++++-- .../registry_factory_test.go | 50 +++++++ 3 files changed, 190 insertions(+), 8 deletions(-) diff --git a/docs/customresourcestate-metrics.md b/docs/customresourcestate-metrics.md index a448e02b2b..7d2052a75c 100644 --- a/docs/customresourcestate-metrics.md +++ b/docs/customresourcestate-metrics.md @@ -531,6 +531,29 @@ Examples: # For generally matching against a field in an object schema, use the following syntax: [metadata, "name=foo"] # if v, ok := metadata[name]; ok && v == "foo" { return v; } else { /* ignore */ } + +# expand a list +[spec, order, "*", value] # syntax +# for the object snippet below +... +status: + namespaces: + - namespace: "foo" + status: + available: + resourceA: '10' + resourceB: '20' + pending: + resourceA: '0' + resourceB: '6' +... +# resolves to: [status, namespaces, , status] (a multi-dimensional array) +path: [status, namespaces, "*", status] +labelsFromPath: + # this can be combined with the wildcard prefixes feature introduced in #2052 + "available_*": [available] +# outputs: +...{...,available_resourceA="10",available_resourceB="20",...} ... ``` ### Wildcard matching of version and kind fields diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index 09e678a6c3..b79c1adda3 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -128,12 +128,22 @@ type compiledCommon struct { t metric.Type } +func (c *compiledCommon) SetPath(p valuePath) { + c.path = p +} + func (c compiledCommon) Path() valuePath { return c.path } + +func (c *compiledCommon) SetLabelFromPath(parr map[string]valuePath) { + c.labelFromPath = parr +} + func (c compiledCommon) LabelFromPath() map[string]valuePath { return c.labelFromPath } + func (c compiledCommon) Type() metric.Type { return c.t } @@ -146,7 +156,9 @@ type eachValue struct { type compiledMetric interface { Values(v interface{}) (result []eachValue, err []error) Path() valuePath + SetPath(valuePath) LabelFromPath() map[string]valuePath + SetLabelFromPath(map[string]valuePath) Type() metric.Type } @@ -554,11 +566,35 @@ type pathOp struct { type valuePath []pathOp func (p valuePath) Get(obj interface{}) interface{} { + handleNil := func(object interface{}, part string) interface{} { + switch tobj := object.(type) { + case map[string]interface{}: + return tobj[part] + case []interface{}: + if part == "*" { + return tobj + } + idx, err := strconv.Atoi(part) + if err != nil { + return nil + } + if idx < 0 || idx >= len(tobj) { + return nil + } + return tobj[idx] + default: + return nil + } + } for _, op := range p { if obj == nil { return nil } - obj = op.op(obj) + if op.op == nil { + obj = handleNil(obj, op.part) + } else { + obj = op.op(obj) + } } return obj } @@ -674,22 +710,95 @@ func famGen(f compiledFamily) generator.FamilyGenerator { } } +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 ; j < i; j++ { + maybeCheckpoint, ok := checkpoint[path[j].part] + if !ok { + // path[j] is not in the object, so we can't expand the wildcard + return []valuePath{path} + } + // store (persist) last checkpoint + if c, ok := maybeCheckpoint.([]interface{}); ok { + list = c + break + } + checkpoint = maybeCheckpoint.(map[string]interface{}) + } + if j > i { + break + } + // i is at "*", j is at the last part before "*", checkpoint is at the value of the last part before "*" + l = len(list) // number of elements in the list + pathCopyStart := make(valuePath, i) + copy(pathCopyStart, path[:i]) + pathCopyWildcard := make(valuePath, len(path)-i-1) + copy(pathCopyWildcard, path[i+1:]) + for k := 0; k < l; k++ { + pathCopyStart = append(pathCopyStart, pathOp{part: strconv.Itoa(k)}) + pathCopyStart = append(pathCopyStart, pathCopyWildcard...) + expandedPaths = append(expandedPaths, pathCopyStart) + } + j++ // skip "*" + } + return expandedPaths[:l] +} + // generate generates the metrics for a custom resource. func generate(u *unstructured.Unstructured, f compiledFamily, errLog klog.Verbose) *metric.Family { 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) + } - values, errors := scrapeValuesFor(f.Each, u.Object) - for _, err := range errors { - 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() + } } - for _, v := range values { - v.DefaultLabels(baseLabels) - metrics = append(metrics, v.ToMetric()) + if f.Each.LabelFromPath() != nil { + labelsFromPath := make(map[string]valuePath) + flfp := f.Each.LabelFromPath() + for k, flfpPath := range flfp { + fLPaths := resolveWildcard(flfpPath, u.Object) + for i, fPath := range fLPaths { + genLabel := k + strconv.Itoa(i) + labelsFromPath[genLabel] = fPath + } + } + if len(labelsFromPath) > 0 { + f.Each.SetLabelFromPath(labelsFromPath) + } + fn() } - 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 7f5d1a0928..25f3621aa1 100644 --- a/pkg/customresourcestate/registry_factory_test.go +++ b/pkg/customresourcestate/registry_factory_test.go @@ -41,10 +41,20 @@ func init() { Obj{ "id": 1, "value": true, + "arr": Array{ + Obj{ + "foo": "bar", + }, + }, }, Obj{ "id": 3, "value": false, + "arr": Array{ + Obj{ + "foo": "baz", + }, + }, }, }, }, @@ -531,6 +541,46 @@ func Test_valuePath_Get(t *testing.T) { } } +func Test_resolveWildcard(t *testing.T) { + tests := []struct { + path valuePath + want []valuePath + name string + }{ + { + name: "wildcard not at the boundary", + path: mustCompilePath(t, "spec", "order", "*", "value"), + want: []valuePath{ + mustCompilePath(t, "spec", "order", "0", "value"), + mustCompilePath(t, "spec", "order", "1", "value"), + }, + }, + { + name: "wildcard at the boundary", + path: mustCompilePath(t, "spec", "order", "*"), + want: []valuePath{ + mustCompilePath(t, "spec", "order", "0"), + mustCompilePath(t, "spec", "order", "1"), + }, + }, + { + name: "multiple wildcards", + path: mustCompilePath(t, "spec", "order", "*", "arr", "*", "foo"), + want: []valuePath{ + mustCompilePath(t, "spec", "order", "0", "arr", "0", "foo"), + mustCompilePath(t, "spec", "order", "1", "arr", "0", "foo"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveWildcard(tt.path, cr) + reflect.DeepEqual(got, tt.want) + }) + } +} + func newEachValue(t *testing.T, value float64, labels ...string) eachValue { t.Helper() if len(labels)%2 != 0 {