Skip to content

Commit e997719

Browse files
authored
add option to configure histogram buckets (#164)
1 parent cbc2df5 commit e997719

File tree

3 files changed

+56
-6
lines changed

3 files changed

+56
-6
lines changed

status/controller.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type Option struct {
6262
GaugeMetricLabels []string
6363
MetricFields map[string]string
6464
GaugeMetricFields map[string]string
65+
HistogramBuckets []float64
6566
}
6667

6768
func EmitDeprecatedMetrics(o *Option) {
@@ -92,6 +93,12 @@ func WithGaugeFields(fields map[string]string) func(*Option) {
9293
}
9394
}
9495

96+
func WithHistogramBuckets(buckets []float64) func(*Option) {
97+
return func(o *Option) {
98+
o.HistogramBuckets = buckets
99+
}
100+
}
101+
95102
func NewController[T Object](client client.Client, eventRecorder record.EventRecorder, opts ...option.Function[Option]) *Controller[T] {
96103
options := option.Resolve(opts...)
97104
obj := reflect.New(reflect.TypeOf(*new(T)).Elem()).Interface().(runtime.Object)
@@ -107,7 +114,7 @@ func NewController[T Object](client client.Client, eventRecorder record.EventRec
107114
kubeClient: client,
108115
eventRecorder: eventRecorder,
109116
emitDeprecatedMetrics: options.EmitDeprecatedMetrics,
110-
ConditionDuration: conditionDurationMetric(strings.ToLower(gvk.Kind), lo.Map(
117+
ConditionDuration: conditionDurationMetric(strings.ToLower(gvk.Kind), options.HistogramBuckets, lo.Map(
111118
append(options.MetricLabels, lo.Keys(options.MetricFields)...),
112119
func(k string, _ int) string { return toPrometheusLabel(k) })...),
113120
ConditionCount: conditionCountMetric(strings.ToLower(gvk.Kind), lo.Map(
@@ -128,7 +135,7 @@ func NewController[T Object](client client.Client, eventRecorder record.EventRec
128135
append(lo.Keys(options.MetricFields), lo.Keys(options.GaugeMetricFields)...),
129136
append(options.MetricLabels, options.GaugeMetricLabels...)...,
130137
), func(k string, _ int) string { return toPrometheusLabel(k) })...),
131-
TerminationDuration: terminationDurationMetric(strings.ToLower(gvk.Kind), lo.Map(
138+
TerminationDuration: terminationDurationMetric(strings.ToLower(gvk.Kind), options.HistogramBuckets, lo.Map(
132139
append(options.MetricLabels, lo.Keys(options.MetricFields)...),
133140
func(k string, _ int) string { return toPrometheusLabel(k) })...),
134141
}

status/controller_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,45 @@ var _ = Describe("Controller", func() {
763763
Entry("when using WithFields", status.WithFields(map[string]string{"operator.pkg/key1": ".spec.field1", "operator.pkg/key2": ".spec.field2", "operator.pkg/key3": ".spec.field3"}), false),
764764
Entry("when using WithGaugeFields", status.WithGaugeFields(map[string]string{"operator.pkg/key1": ".spec.field1", "operator.pkg/key2": ".spec.field2", "operator.pkg/key3": ".spec.field3"}), true),
765765
)
766+
It("should use custom histogram buckets when specified", func() {
767+
customBuckets := []float64{0.1, 0.5, 1.0, 2.0, 5.0}
768+
metrics.Registry = prometheus.NewRegistry()
769+
770+
controller = status.NewController[*test.CustomObject](kubeClient, recorder, status.WithHistogramBuckets(customBuckets))
771+
772+
testObject := test.Object(&test.CustomObject{})
773+
testObject.StatusConditions() // initialize conditions
774+
775+
// Apply object and reconcile to set initial state
776+
ExpectApplied(ctx, kubeClient, testObject)
777+
ExpectReconciled(ctx, controller, testObject)
778+
779+
// Wait a bit to ensure some time passes for duration measurement
780+
time.Sleep(100 * time.Millisecond)
781+
782+
// Transition a condition to trigger histogram observation
783+
testObject.StatusConditions().SetTrue(test.ConditionTypeFoo)
784+
ExpectApplied(ctx, kubeClient, testObject)
785+
ExpectReconciled(ctx, controller, testObject)
786+
787+
// Verify that the histogram metric exists and has data
788+
metric := GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(test.ConditionTypeFoo, metav1.ConditionUnknown))
789+
Expect(metric).ToNot(BeNil())
790+
791+
histogram := metric.GetHistogram()
792+
Expect(histogram).ToNot(BeNil())
793+
Expect(histogram.GetSampleCount()).To(BeNumerically(">", 0))
794+
795+
// Verify custom buckets are being used by checking bucket count matches our custom buckets
796+
// The histogram should have len(customBuckets) + 1 buckets (including +Inf)
797+
buckets := histogram.GetBucket()
798+
Expect(len(buckets)).To(Equal(len(customBuckets)))
799+
800+
// Verify the bucket upper bounds match our custom buckets
801+
for i, bucket := range buckets {
802+
Expect(bucket.GetUpperBound()).To(Equal(customBuckets[i]))
803+
}
804+
})
766805
})
767806

768807
var _ = Describe("Generic Controller", func() {

status/metrics.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ const (
2121
)
2222

2323
// Cardinality is limited to # objects * # conditions * # objectives
24-
var ConditionDuration = conditionDurationMetric("", pmetrics.LabelGroup, pmetrics.LabelKind)
24+
var ConditionDuration = conditionDurationMetric("", nil, pmetrics.LabelGroup, pmetrics.LabelKind)
2525

26-
func conditionDurationMetric(objectName string, additionalLabels ...string) pmetrics.ObservationMetric {
26+
func conditionDurationMetric(objectName string, buckets []float64, additionalLabels ...string) pmetrics.ObservationMetric {
2727
subsystem := lo.Ternary(len(objectName) == 0, MetricSubsystem, fmt.Sprintf("%s_%s", objectName, MetricSubsystem))
28+
buckets = lo.Ternary(len(buckets) == 0, prometheus.DefBuckets, buckets)
2829

2930
return pmetrics.NewPrometheusHistogram(
3031
metrics.Registry,
@@ -33,6 +34,7 @@ func conditionDurationMetric(objectName string, additionalLabels ...string) pmet
3334
Subsystem: subsystem,
3435
Name: "transition_seconds",
3536
Help: "The amount of time a condition was in a given state before transitioning. e.g. Alarm := P99(Updated=False) > 5 minutes",
37+
Buckets: buckets,
3638
},
3739
append([]string{
3840
pmetrics.LabelType,
@@ -134,10 +136,11 @@ func terminationCurrentTimeSecondsMetric(objectName string, additionalLabels ...
134136
)
135137
}
136138

137-
var TerminationDuration = terminationDurationMetric("", pmetrics.LabelGroup, pmetrics.LabelKind)
139+
var TerminationDuration = terminationDurationMetric("", nil, pmetrics.LabelGroup, pmetrics.LabelKind)
138140

139-
func terminationDurationMetric(objectName string, additionalLabels ...string) pmetrics.ObservationMetric {
141+
func terminationDurationMetric(objectName string, buckets []float64, additionalLabels ...string) pmetrics.ObservationMetric {
140142
subsystem := lo.Ternary(len(objectName) == 0, TerminationSubsystem, fmt.Sprintf("%s_%s", objectName, TerminationSubsystem))
143+
buckets = lo.Ternary(len(buckets) == 0, prometheus.DefBuckets, buckets)
141144

142145
return pmetrics.NewPrometheusHistogram(
143146
metrics.Registry,
@@ -146,6 +149,7 @@ func terminationDurationMetric(objectName string, additionalLabels ...string) pm
146149
Subsystem: subsystem,
147150
Name: "duration_seconds",
148151
Help: "The amount of time taken by an object to terminate completely.",
152+
Buckets: buckets,
149153
},
150154
additionalLabels,
151155
)

0 commit comments

Comments
 (0)