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 Aug 29, 2023
1 parent 13d6a05 commit 0f91e62
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 8 deletions.
23 changes: 23 additions & 0 deletions docs/customresourcestate-metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, <range len(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
Expand Down
125 changes: 117 additions & 8 deletions pkg/customresourcestate/registry_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}

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

0 comments on commit 0f91e62

Please sign in to comment.