Skip to content

Commit 6d2a555

Browse files
committed
Allow labelFromKey field
Allow `labelFromKey` field for the following types: * Gauge: Done. * Info: Done. * StateSet: N/A (redundant use case, see doc changes for more info). Signed-off-by: Pranshu Srivastava <[email protected]>
1 parent 086a6fd commit 6d2a555

File tree

4 files changed

+67
-34
lines changed

4 files changed

+67
-34
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: 51 additions & 30 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,8 @@ 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 key != "" && c.labelFromKey != "" {
218+
ev.Labels[c.labelFromKey] = key
232219
}
233220
addPathLabels(it, c.LabelFromPath(), ev.Labels)
234221
result = append(result, *ev)
@@ -257,22 +244,54 @@ func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error)
257244

258245
type compiledInfo struct {
259246
compiledCommon
247+
labelFromKey string
260248
}
261249

262250
func (c *compiledInfo) Values(v interface{}) (result []eachValue, errs []error) {
263-
if vs, isArray := v.([]interface{}); isArray {
264-
for _, obj := range vs {
251+
onError := func(err ...error) {
252+
errs = append(errs, fmt.Errorf("%s: %v", c.Path(), err))
253+
}
254+
255+
switch iter := v.(type) {
256+
case []interface{}:
257+
for _, obj := range iter {
265258
ev, err := c.values(obj)
266259
if len(err) > 0 {
267-
errs = append(errs, err...)
260+
onError(err...)
268261
continue
269262
}
270263
result = append(result, ev...)
271264
}
272-
return
265+
default:
266+
value, err := c.values(v)
267+
if err != nil {
268+
onError(err...)
269+
break
270+
}
271+
// labelFromKey logic
272+
if vv, ok := v.(map[string]interface{}); ok {
273+
for key, val := range vv {
274+
if key != "" && c.labelFromKey != "" {
275+
n, err := toFloat64(val, false)
276+
if err != nil {
277+
onError(err)
278+
continue
279+
}
280+
result = append(result, eachValue{
281+
Labels: map[string]string{
282+
c.labelFromKey: key,
283+
},
284+
Value: n,
285+
})
286+
}
287+
}
288+
}
289+
if len(result) == 0 {
290+
result = value
291+
}
273292
}
274293

275-
return c.values(v)
294+
return
276295
}
277296

278297
func (c *compiledInfo) values(v interface{}) (result []eachValue, err []error) {
@@ -355,7 +374,7 @@ func less(a, b map[string]string) bool {
355374

356375
func (c compiledGauge) value(it interface{}) (*eachValue, error) {
357376
labels := make(map[string]string)
358-
value, err := getNum(c.ValueFrom.Get(it), c.NilIsZero)
377+
value, err := toFloat64(c.ValueFrom.Get(it), c.NilIsZero)
359378
if err != nil {
360379
return nil, fmt.Errorf("%s: %w", c.ValueFrom, err)
361380
}
@@ -478,7 +497,7 @@ func compilePath(path []string) (out valuePath, _ error) {
478497
return nil, fmt.Errorf("invalid list lookup: %s", part)
479498
}
480499
key, val := eq[0], eq[1]
481-
num, notNum := getNum(val, false)
500+
num, notNum := toFloat64(val, false)
482501
boolVal, notBool := strconv.ParseBool(val)
483502
out = append(out, pathOp{
484503
part: part,
@@ -496,7 +515,7 @@ func compilePath(path []string) (out valuePath, _ error) {
496515
}
497516

498517
if notNum == nil {
499-
if i, err := getNum(candidate, false); err == nil && num == i {
518+
if i, err := toFloat64(candidate, false); err == nil && num == i {
500519
return m
501520
}
502521
}
@@ -522,13 +541,14 @@ func compilePath(path []string) (out valuePath, _ error) {
522541
} else if s, ok := m.([]interface{}); ok {
523542
i, err := strconv.Atoi(part)
524543
if err != nil {
525-
return nil
544+
return fmt.Errorf("invalid list index: %s", part)
526545
}
527546
if i < 0 {
547+
// negative index
528548
i += len(s)
529549
}
530550
if !(0 <= i && i < len(s)) {
531-
return nil
551+
return fmt.Errorf("list index out of range: %s", part)
532552
}
533553
return s[i]
534554
}
@@ -544,6 +564,7 @@ func famGen(f compiledFamily) generator.FamilyGenerator {
544564
errLog := klog.V(f.ErrorLogV)
545565
return generator.FamilyGenerator{
546566
Name: f.Name,
567+
// TODO(@rexagod): This should be dynamic.
547568
Type: metric.Gauge,
548569
Help: f.Help,
549570
GenerateFunc: func(obj interface{}) *metric.Family {
@@ -585,8 +606,8 @@ func scrapeValuesFor(e compiledEach, obj map[string]interface{}) ([]eachValue, [
585606
return result, errs
586607
}
587608

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) {
609+
// toFloat64 converts the value to a float64 which is the value type for any metric.
610+
func toFloat64(value interface{}, nilIsZero bool) (float64, error) {
590611
var v float64
591612
// same as bool==false but for bool pointers
592613
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, 3, "type", "type-b"),
225+
}},
217226
{name: "stateset", each: &compiledStateSet{
218227
compiledCommon: compiledCommon{
219228
path: mustCompilePath(t, "status", "phase"),

0 commit comments

Comments
 (0)