diff --git a/docs/customresourcestate-metrics.md b/docs/customresourcestate-metrics.md index 0ed35f494b..144a0513cb 100644 --- a/docs/customresourcestate-metrics.md +++ b/docs/customresourcestate-metrics.md @@ -480,4 +480,7 @@ Examples: # if the value to be matched is a number or boolean, the value is compared as a number or boolean [status, conditions, "[value=66]", name] # status.conditions[1].name = "b" + +# expand a list +[spec, order, "*", value] # spec.order[*].value = true ``` diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index 15ebdcbe72..02440f85cd 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -124,12 +124,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 } @@ -142,7 +152,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 } @@ -541,11 +553,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 } @@ -651,22 +687,97 @@ 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 + switch maybeCheckpoint.(type) { + case []interface{}: + list = maybeCheckpoint.([]interface{}) + break + case map[string]interface{}: + 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 + pathCopyPrev := make(valuePath, i) + copy(pathCopyPrev, path[:i]) + pathCopyNext := make(valuePath, len(path)-i-1) + copy(pathCopyNext, path[i+1:]) + for k := 0; k < l; k++ { + t := append(pathCopyPrev, pathOp{part: strconv.Itoa(k)}) + tt := append(t, pathCopyNext...) + expandedPaths = append(expandedPaths, tt) + } + 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 { + kGen := k + strconv.Itoa(i) + labelsFromPath[kGen] = 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 d68f22b617..4685baf331 100644 --- a/pkg/customresourcestate/registry_factory_test.go +++ b/pkg/customresourcestate/registry_factory_test.go @@ -502,6 +502,38 @@ 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"), + }, + }, + } + + 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 {