Skip to content

Commit

Permalink
Support list expansions
Browse files Browse the repository at this point in the history
Allow CRS configuration to take in path values for lists, denoted by
"*". This means it's possible to specify `[..., <list>, "*", foo, ...]`
in the configuration to dynamically generate multiple metrics that
reflect different states of `foo` in various list elements.
  • Loading branch information
rexagod committed May 16, 2023
1 parent 3b95dd1 commit 5586928
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 8 deletions.
3 changes: 3 additions & 0 deletions docs/customresourcestate-metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
127 changes: 119 additions & 8 deletions pkg/customresourcestate/registry_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 32 additions & 0 deletions pkg/customresourcestate/registry_factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 5586928

Please sign in to comment.