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) {

Check failure on line 714 in pkg/customresourcestate/registry_factory.go

View workflow job for this annotation

GitHub Actions / ci-go-lint

typeSwitchVar: 2 cases can benefit from type switch with assignment (gocritic)
case []interface{}:
list = maybeCheckpoint.([]interface{})

Check failure on line 716 in pkg/customresourcestate/registry_factory.go

View workflow job for this annotation

GitHub Actions / ci-go-lint

S1034(related information): could eliminate this type assertion (gosimple)
break

Check failure on line 717 in pkg/customresourcestate/registry_factory.go

View workflow job for this annotation

GitHub Actions / ci-go-lint

S1023: redundant break statement (gosimple)
case map[string]interface{}:
checkpoint = maybeCheckpoint.(map[string]interface{})

Check failure on line 719 in pkg/customresourcestate/registry_factory.go

View workflow job for this annotation

GitHub Actions / ci-go-lint

S1034(related information): could eliminate this type assertion (gosimple)
}
}
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)})

Check failure on line 732 in pkg/customresourcestate/registry_factory.go

View workflow job for this annotation

GitHub Actions / ci-go-lint

appendAssign: append result not assigned to the same slice (gocritic)
tt := append(t, pathCopyNext...)

Check failure on line 733 in pkg/customresourcestate/registry_factory.go

View workflow job for this annotation

GitHub Actions / ci-go-lint

appendAssign: append result not assigned to the same slice (gocritic)
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)

Check failure on line 772 in pkg/customresourcestate/registry_factory.go

View workflow job for this annotation

GitHub Actions / ci-go-lint

var-naming: don't use leading k in Go names; var kGen should be gen (revive)
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.