Skip to content

Commit db2d496

Browse files
authored
validate the metric naming of gaugevecset (#5)
1 parent c35dcde commit db2d496

File tree

5 files changed

+100
-23
lines changed

5 files changed

+100
-23
lines changed

README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,12 @@ comes instead of `meta.SetStatusCondition`.
206206
To delete the metrics for a given custom resource, simply call `RemoveConditionsFor` and pass the object.
207207

208208
```go
209-
const (
210-
kind = "MyCr"
211-
)
212-
213209
// SetStatusCondition utility function which replaces and wraps meta.SetStatusCondition calls
214210
func (r *MyReconciler) SetStatusCondition(cr *v1.MyCR, condition metav1.Condition) bool {
215-
changed = meta.SetStatusCondition(&cr.Status.Conditions, condition)
211+
changed := meta.SetStatusCondition(&cr.Status.Conditions, condition)
212+
kind := cr.GetObjectKind().GroupVersionKind().Kind
216213
if changed {
217-
r.RecordConditionFor(kind, cr, condition.Type, string(condition.Status), condition.Reason)
214+
r.Recorder.RecordConditionFor(kind, cr, condition.Type, string(condition.Status), condition.Reason)
218215
}
219216
return changed
220217
}
@@ -228,6 +225,7 @@ func (r *MyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Re
228225

229226
// Remove the metrics when the CR is deleted
230227
if cr.DeletionTimeStamp != nil {
228+
kind := cr.GetObjectKind().GroupVersionKind().Kind
231229
r.Recorder.RemoveConditionsFor(kind, cr)
232230
}
233231

pkg/gauge_vec_set/gauge_vec_set.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gauge_vec_set
22

33
import (
44
"fmt"
5+
"regexp"
56
"strings"
67
"sync"
78

@@ -53,6 +54,26 @@ type GaugeVecSet struct {
5354
mu sync.RWMutex
5455
}
5556

57+
// validateLowercaseUnderscore panics if the input contains any character
58+
// that is not a lowercase letter or underscore.
59+
func validateLowercaseUnderscore(input string) {
60+
invalidCharPattern := regexp.MustCompile(`[^a-z_]`)
61+
if invalidCharPattern.MatchString(input) {
62+
panic(
63+
fmt.Errorf(
64+
"NewGaugeVecSet: %q contains characters other than lowercase letters and underscores", input,
65+
),
66+
)
67+
}
68+
if strings.HasSuffix(input, "_") {
69+
panic(
70+
fmt.Errorf(
71+
"NewGaugeVecSet: %q must not end with an underscore", input,
72+
),
73+
)
74+
}
75+
}
76+
5677
// NewGaugeVecSet constructs a GaugeVecSet.
5778
//
5879
// Parameters:
@@ -74,6 +95,10 @@ func NewGaugeVecSet(
7495
groupLabels []string,
7596
extraLabels ...string,
7697
) *GaugeVecSet {
98+
validateLowercaseUnderscore(namespace)
99+
validateLowercaseUnderscore(subsystem)
100+
validateLowercaseUnderscore(name)
101+
77102
if len(indexLabels) == 0 {
78103
panic("NewMultiIndexGaugeCollector: at least one index label is required")
79104
}

pkg/gauge_vec_set/gauge_vec_set_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,60 @@ func Test_DynamicGaugeCollector_ArityValidationPanics(t *testing.T) {
350350
})
351351
}
352352

353+
func Test_DynamicGaugeCollector_MetricNamePanics(t *testing.T) {
354+
cases := []struct {
355+
name string
356+
namespace string
357+
subsystem string
358+
metricName string
359+
}{
360+
{
361+
name: "invalid namespace contains special characters",
362+
namespace: "n-s",
363+
subsystem: "subsystem",
364+
metricName: "name",
365+
},
366+
{
367+
name: "invalid subsystem contains special characters",
368+
namespace: "namespace",
369+
subsystem: "sub-system",
370+
metricName: "name",
371+
},
372+
{
373+
name: "invalid metric contains special characters",
374+
namespace: "namespace",
375+
subsystem: "subsystem",
376+
metricName: "na-me",
377+
},
378+
{
379+
name: "invalid namespace ends with underscore",
380+
namespace: "namespace_",
381+
subsystem: "subsystem",
382+
metricName: "name",
383+
},
384+
{
385+
name: "invalid subsystem ends with underscore",
386+
namespace: "namespace",
387+
subsystem: "subsystem_",
388+
metricName: "name",
389+
},
390+
{
391+
name: "invalid metric ends with underscore",
392+
namespace: "namespace",
393+
subsystem: "subsystem",
394+
metricName: "name_",
395+
},
396+
}
397+
398+
for _, c := range cases {
399+
t.Run(c.name, func(t *testing.T) {
400+
assert.Panics(t, func() {
401+
NewGaugeVecSet(c.namespace, c.subsystem, c.metricName, "", []string{"index"}, []string{"group"})
402+
})
403+
})
404+
}
405+
}
406+
353407
func Test_DynamicGaugeCollector_LabelsWithHashCharacters(t *testing.T) {
354408
reg := prometheus.NewRegistry()
355409

pkg/operator_condition_metrics/operator_condition_metrics.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ and marking exactly one as active (1) while the others are inactive (0). Example
1414
kube_pod_status_phase{namespace="default", pod="nginx", phase="Pending"} 0
1515
kube_pod_status_phase{namespace="default", pod="nginx", phase="Failed"} 0
1616
17-
We adopt the same pattern for controller Conditions but we export one time series per (status, reason) variant
17+
We adopt the same pattern for controller Conditions, but we export one time series per (status, reason) variant
1818
and enforce **exclusivity per condition**.
1919
2020
For any given (controller, kind, name, namespace, condition) exactly one (status, reason) series is present at a time.
@@ -24,13 +24,13 @@ Metric
2424
<namespace>_controller_condition
2525
2626
Labels (order matches registration)
27-
- controller: controller name (e.g., "my-operator")
28-
- kind: resource kind (e.g., "MyCRD")
29-
- name: resource name
30-
- namespace: resource namespace ("" for cluster-scoped)
31-
- condition: condition type (e.g., "Ready", "Reconciled")
32-
- status: "True" | "False" | "Unknown"
33-
- reason: short machine-typed reason (often "" when status="True")
27+
- controller: controller name (e.g., "my-operator")
28+
- resource_kind: resource kind (e.g., "MyCRD")
29+
- resource_name: resource name
30+
- resource_namespace: resource namespace ("" for cluster-scoped)
31+
- condition: condition type (e.g., "Ready", "Reconciled")
32+
- status: "True" | "False" | "Unknown"
33+
- reason: short machine-typed reason (often "" when status="True")
3434
3535
Value
3636
- Always 1 for the single active (status, reason) series in the group.
@@ -41,9 +41,9 @@ Examples:
4141
4242
my_controller_condition{
4343
controller="my-operator",
44-
kind="MyCRD",
45-
name="my-cr-1",
46-
namespace="prod",
44+
resource_kind="MyCRD",
45+
resource_name="my-cr-1",
46+
resource_namespace="prod",
4747
condition="Ready",
4848
status="True",
4949
reason=""
@@ -73,11 +73,11 @@ Examples:
7373
7474
Cleanup
7575
When the resource is deleted/pruned, all series for its index key
76-
(controller, kind, name, namespace) are removed via DeleteByIndex().
76+
(controller, kind, resource_name, resource_namespace) are removed via DeleteByIndex().
7777
7878
Implementation
7979
Backed by a GaugeVecSet with:
80-
indexLabels = [controller, kind, name, namespace]
80+
indexLabels = [controller, resource_kind, resource_name, resource_namespace]
8181
groupLabels = [condition]
8282
extraLabels = [status, reason]
8383
Exclusivity is enforced with SetGroup(), which deletes sibling series.
@@ -90,7 +90,7 @@ const (
9090
)
9191

9292
var (
93-
indexLabels = []string{"controller", "kind", "name", "namespace"}
93+
indexLabels = []string{"controller", "resource_kind", "resource_name", "resource_namespace"}
9494
groupLabels = []string{"condition"}
9595
extraLabels = []string{"status", "reason"}
9696
)

pkg/operator_condition_metrics/operator_condition_metrics_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ func TestConditionMetricRecorder_Record_Transition_And_SecondCondition(t *testin
4747
want := `
4848
# HELP test_record_transition_and_second_condition_controller_condition Condition status for a custom resource; one active (status,reason) time series per (controller,kind,name,namespace,condition).
4949
# TYPE test_record_transition_and_second_condition_controller_condition gauge
50-
test_record_transition_and_second_condition_controller_condition{condition="Ready",controller="my-controller",kind="MyCRD",name="cr-1",namespace="prod",reason="Failed",status="False"} 1
51-
test_record_transition_and_second_condition_controller_condition{condition="Synchronized",controller="my-controller",kind="MyCRD",name="cr-1",namespace="prod",reason="",status="True"} 1
50+
test_record_transition_and_second_condition_controller_condition{condition="Ready",controller="my-controller",reason="Failed",resource_kind="MyCRD",resource_name="cr-1",resource_namespace="prod",status="False"} 1
51+
test_record_transition_and_second_condition_controller_condition{condition="Synchronized",controller="my-controller",reason="",resource_kind="MyCRD",resource_name="cr-1",resource_namespace="prod",status="True",} 1
5252
`
5353
require.NoError(t,
5454
testutil.GatherAndCompare(
@@ -114,7 +114,7 @@ func TestConditionMetricRecorder_SetsKindLabelFromObject(t *testing.T) {
114114
want := `
115115
# HELP test_sets_kind_label_from_object_controller_condition Condition status for a custom resource; one active (status,reason) time series per (controller,kind,name,namespace,condition).
116116
# TYPE test_sets_kind_label_from_object_controller_condition gauge
117-
test_sets_kind_label_from_object_controller_condition{condition="Ready",controller="my-controller",kind="FancyKind",name="obj-1",namespace="ns-1",reason="",status="True"} 1
117+
test_sets_kind_label_from_object_controller_condition{condition="Ready",controller="my-controller",reason="",resource_kind="FancyKind",resource_name="obj-1",resource_namespace="ns-1",status="True"} 1
118118
`
119119
require.NoError(t,
120120
testutil.GatherAndCompare(

0 commit comments

Comments
 (0)