Skip to content

Commit b2c90e8

Browse files
authored
Merge pull request #1880 from rexagod/1871-1868
Allow labelFromKey field for all applicable types
2 parents 9edd02b + 3072502 commit b2c90e8

File tree

4 files changed

+73
-35
lines changed

4 files changed

+73
-35
lines changed

docs/customresourcestate-metrics.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ spec:
146146
path: [status, sub]
147147
148148
# if path targets an object, the object key will be used as label value
149+
# This is not supported for StateSet type as all values will be truthy, which is redundant.
149150
labelFromKey: type
150151
# label values can be resolved specific to this path
151152
labelsFromPath:

pkg/customresourcestate/config_metrics_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ type MetricGauge struct {
5151
// Ref: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#info
5252
type MetricInfo struct {
5353
MetricMeta `yaml:",inline" json:",inline"`
54+
// LabelFromKey adds a label with the given name if Path is an object. The label value will be the object key.
55+
LabelFromKey string `yaml:"labelFromKey" json:"labelFromKey"`
5456
}
5557

5658
// MetricStateSet is a metric which represent a series of related boolean values, also called a bitset.

pkg/customresourcestate/registry_factory.go

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ func newCompiledMetric(m Metric) (compiledMetric, error) {
157157
compiledCommon: *cc,
158158
ValueFrom: valueFromPath,
159159
NilIsZero: m.Gauge.NilIsZero,
160+
labelFromKey: m.Gauge.LabelFromKey,
160161
}, nil
161162
case MetricTypeInfo:
162163
if m.Info == nil {
@@ -168,6 +169,7 @@ func newCompiledMetric(m Metric) (compiledMetric, error) {
168169
}
169170
return &compiledInfo{
170171
compiledCommon: *cc,
172+
labelFromKey: m.Info.LabelFromKey,
171173
}, nil
172174
case MetricTypeStateSet:
173175
if m.StateSet == nil {
@@ -195,23 +197,8 @@ func newCompiledMetric(m Metric) (compiledMetric, error) {
195197
type compiledGauge struct {
196198
compiledCommon
197199
ValueFrom valuePath
198-
LabelFromKey string
199200
NilIsZero bool
200-
}
201-
202-
func newCompiledGauge(m *MetricGauge) (*compiledGauge, error) {
203-
cc, err := compileCommon(m.MetricMeta)
204-
if err != nil {
205-
return nil, fmt.Errorf("compile common: %w", err)
206-
}
207-
valueFromPath, err := compilePath(m.ValueFrom)
208-
if err != nil {
209-
return nil, fmt.Errorf("compile path ValueFrom: %w", err)
210-
}
211-
return &compiledGauge{
212-
compiledCommon: *cc,
213-
ValueFrom: valueFromPath,
214-
}, nil
201+
labelFromKey string
215202
}
216203

217204
func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error) {
@@ -227,8 +214,12 @@ func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error)
227214
onError(fmt.Errorf("[%s]: %w", key, err))
228215
continue
229216
}
230-
if key != "" && c.LabelFromKey != "" {
231-
ev.Labels[c.LabelFromKey] = key
217+
if _, ok := ev.Labels[c.labelFromKey]; ok {
218+
onError(fmt.Errorf("labelFromKey (%s) generated labels conflict with labelsFromPath, consider renaming it", c.labelFromKey))
219+
continue
220+
}
221+
if key != "" && c.labelFromKey != "" {
222+
ev.Labels[c.labelFromKey] = key
232223
}
233224
addPathLabels(it, c.LabelFromPath(), ev.Labels)
234225
result = append(result, *ev)
@@ -257,22 +248,53 @@ func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error)
257248

258249
type compiledInfo struct {
259250
compiledCommon
251+
labelFromKey string
260252
}
261253

262254
func (c *compiledInfo) Values(v interface{}) (result []eachValue, errs []error) {
263-
if vs, isArray := v.([]interface{}); isArray {
264-
for _, obj := range vs {
255+
onError := func(err ...error) {
256+
errs = append(errs, fmt.Errorf("%s: %v", c.Path(), err))
257+
}
258+
259+
switch iter := v.(type) {
260+
case []interface{}:
261+
for _, obj := range iter {
265262
ev, err := c.values(obj)
266263
if len(err) > 0 {
267-
errs = append(errs, err...)
264+
onError(err...)
268265
continue
269266
}
270267
result = append(result, ev...)
271268
}
272-
return
269+
case map[string]interface{}:
270+
value, err := c.values(v)
271+
if err != nil {
272+
onError(err...)
273+
break
274+
}
275+
for _, ev := range value {
276+
if _, ok := ev.Labels[c.labelFromKey]; ok {
277+
onError(fmt.Errorf("labelFromKey (%s) generated labels conflict with labelsFromPath, consider renaming it", c.labelFromKey))
278+
continue
279+
}
280+
}
281+
// labelFromKey logic
282+
for key := range iter {
283+
if key != "" && c.labelFromKey != "" {
284+
result = append(result, eachValue{
285+
Labels: map[string]string{
286+
c.labelFromKey: key,
287+
},
288+
Value: 1,
289+
})
290+
}
291+
}
292+
result = append(result, value...)
293+
default:
294+
result, errs = c.values(v)
273295
}
274296

275-
return c.values(v)
297+
return
276298
}
277299

278300
func (c *compiledInfo) values(v interface{}) (result []eachValue, err []error) {
@@ -281,7 +303,9 @@ func (c *compiledInfo) values(v interface{}) (result []eachValue, err []error) {
281303
}
282304
value := eachValue{Value: 1, Labels: map[string]string{}}
283305
addPathLabels(v, c.labelFromPath, value.Labels)
284-
result = append(result, value)
306+
if len(value.Labels) != 0 {
307+
result = append(result, value)
308+
}
285309
return
286310
}
287311

@@ -355,7 +379,7 @@ func less(a, b map[string]string) bool {
355379

356380
func (c compiledGauge) value(it interface{}) (*eachValue, error) {
357381
labels := make(map[string]string)
358-
value, err := getNum(c.ValueFrom.Get(it), c.NilIsZero)
382+
value, err := toFloat64(c.ValueFrom.Get(it), c.NilIsZero)
359383
if err != nil {
360384
return nil, fmt.Errorf("%s: %w", c.ValueFrom, err)
361385
}
@@ -478,7 +502,7 @@ func compilePath(path []string) (out valuePath, _ error) {
478502
return nil, fmt.Errorf("invalid list lookup: %s", part)
479503
}
480504
key, val := eq[0], eq[1]
481-
num, notNum := getNum(val, false)
505+
num, notNum := toFloat64(val, false)
482506
boolVal, notBool := strconv.ParseBool(val)
483507
out = append(out, pathOp{
484508
part: part,
@@ -496,7 +520,7 @@ func compilePath(path []string) (out valuePath, _ error) {
496520
}
497521

498522
if notNum == nil {
499-
if i, err := getNum(candidate, false); err == nil && num == i {
523+
if i, err := toFloat64(candidate, false); err == nil && num == i {
500524
return m
501525
}
502526
}
@@ -522,13 +546,14 @@ func compilePath(path []string) (out valuePath, _ error) {
522546
} else if s, ok := m.([]interface{}); ok {
523547
i, err := strconv.Atoi(part)
524548
if err != nil {
525-
return nil
549+
return fmt.Errorf("invalid list index: %s", part)
526550
}
527551
if i < 0 {
552+
// negative index
528553
i += len(s)
529554
}
530555
if !(0 <= i && i < len(s)) {
531-
return nil
556+
return fmt.Errorf("list index out of range: %s", part)
532557
}
533558
return s[i]
534559
}
@@ -544,6 +569,7 @@ func famGen(f compiledFamily) generator.FamilyGenerator {
544569
errLog := klog.V(f.ErrorLogV)
545570
return generator.FamilyGenerator{
546571
Name: f.Name,
572+
// TODO(@rexagod): This should be dynamic.
547573
Type: metric.Gauge,
548574
Help: f.Help,
549575
GenerateFunc: func(obj interface{}) *metric.Family {
@@ -585,8 +611,8 @@ func scrapeValuesFor(e compiledEach, obj map[string]interface{}) ([]eachValue, [
585611
return result, errs
586612
}
587613

588-
// getNum converts the value to a float64 which is the value type for any metric.
589-
func getNum(value interface{}, nilIsZero bool) (float64, error) {
614+
// toFloat64 converts the value to a float64 which is the value type for any metric.
615+
func toFloat64(value interface{}, nilIsZero bool) (float64, error) {
590616
var v float64
591617
// same as bool==false but for bool pointers
592618
if value == nil {

pkg/customresourcestate/registry_factory_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func Test_values(t *testing.T) {
155155
compiledCommon: compiledCommon{
156156
path: mustCompilePath(t, "status", "active"),
157157
},
158-
LabelFromKey: "type",
158+
labelFromKey: "type",
159159
}, wantResult: []eachValue{
160160
newEachValue(t, 1, "type", "type-a"),
161161
newEachValue(t, 3, "type", "type-b"),
@@ -167,7 +167,7 @@ func Test_values(t *testing.T) {
167167
"active": mustCompilePath(t, "active"),
168168
},
169169
},
170-
LabelFromKey: "type",
170+
labelFromKey: "type",
171171
ValueFrom: mustCompilePath(t, "ready"),
172172
}, wantResult: []eachValue{
173173
newEachValue(t, 2, "type", "type-a", "active", "1"),
@@ -201,7 +201,7 @@ func Test_values(t *testing.T) {
201201
newEachValue(t, 0),
202202
}},
203203
{name: "info", each: &compiledInfo{
204-
compiledCommon{
204+
compiledCommon: compiledCommon{
205205
labelFromPath: map[string]valuePath{
206206
"version": mustCompilePath(t, "spec", "version"),
207207
},
@@ -210,10 +210,19 @@ func Test_values(t *testing.T) {
210210
newEachValue(t, 1, "version", "v0.0.0"),
211211
}},
212212
{name: "info nil path", each: &compiledInfo{
213-
compiledCommon{
213+
compiledCommon: compiledCommon{
214214
path: mustCompilePath(t, "does", "not", "exist"),
215215
},
216216
}, wantResult: nil},
217+
{name: "info label from key", each: &compiledInfo{
218+
compiledCommon: compiledCommon{
219+
path: mustCompilePath(t, "status", "active"),
220+
},
221+
labelFromKey: "type",
222+
}, wantResult: []eachValue{
223+
newEachValue(t, 1, "type", "type-a"),
224+
newEachValue(t, 1, "type", "type-b"),
225+
}},
217226
{name: "stateset", each: &compiledStateSet{
218227
compiledCommon: compiledCommon{
219228
path: mustCompilePath(t, "status", "phase"),

0 commit comments

Comments
 (0)