@@ -19,11 +19,16 @@ package testutil
19
19
import (
20
20
"fmt"
21
21
"io"
22
+ "math"
22
23
"reflect"
24
+ "sort"
23
25
"strings"
24
26
27
+ dto "github.com/prometheus/client_model/go"
25
28
"github.com/prometheus/common/expfmt"
26
29
"github.com/prometheus/common/model"
30
+
31
+ "k8s.io/component-base/metrics"
27
32
)
28
33
29
34
var (
@@ -178,3 +183,140 @@ func ValidateMetrics(metrics Metrics, metricName string, expectedLabels ...strin
178
183
}
179
184
return nil
180
185
}
186
+
187
+ // Histogram wraps prometheus histogram DTO (data transfer object)
188
+ type Histogram struct {
189
+ * dto.Histogram
190
+ }
191
+
192
+ // GetHistogramFromGatherer collects a metric from a gatherer implementing k8s.io/component-base/metrics.Gatherer interface.
193
+ // Used only for testing purposes where we need to gather metrics directly from a running binary (without metrics endpoint).
194
+ func GetHistogramFromGatherer (gatherer metrics.Gatherer , metricName string ) (Histogram , error ) {
195
+ var metricFamily * dto.MetricFamily
196
+ m , err := gatherer .Gather ()
197
+ if err != nil {
198
+ return Histogram {}, err
199
+ }
200
+ for _ , mFamily := range m {
201
+ if mFamily .Name != nil && * mFamily .Name == metricName {
202
+ metricFamily = mFamily
203
+ break
204
+ }
205
+ }
206
+
207
+ if metricFamily == nil {
208
+ return Histogram {}, fmt .Errorf ("Metric %q not found" , metricName )
209
+ }
210
+
211
+ if metricFamily .GetMetric () == nil {
212
+ return Histogram {}, fmt .Errorf ("Metric %q is empty" , metricName )
213
+ }
214
+
215
+ if len (metricFamily .GetMetric ()) == 0 {
216
+ return Histogram {}, fmt .Errorf ("Metric %q is empty" , metricName )
217
+ }
218
+
219
+ return Histogram {
220
+ // Histograms are stored under the first index (based on observation).
221
+ // Given there's only one histogram registered per each metric name, accessing
222
+ // the first index is sufficient.
223
+ metricFamily .GetMetric ()[0 ].GetHistogram (),
224
+ }, nil
225
+ }
226
+
227
+ func uint64Ptr (u uint64 ) * uint64 {
228
+ return & u
229
+ }
230
+
231
+ // Bucket of a histogram
232
+ type bucket struct {
233
+ upperBound float64
234
+ count float64
235
+ }
236
+
237
+ func bucketQuantile (q float64 , buckets []bucket ) float64 {
238
+ if q < 0 {
239
+ return math .Inf (- 1 )
240
+ }
241
+ if q > 1 {
242
+ return math .Inf (+ 1 )
243
+ }
244
+
245
+ if len (buckets ) < 2 {
246
+ return math .NaN ()
247
+ }
248
+
249
+ rank := q * buckets [len (buckets )- 1 ].count
250
+ b := sort .Search (len (buckets )- 1 , func (i int ) bool { return buckets [i ].count >= rank })
251
+
252
+ if b == 0 {
253
+ return buckets [0 ].upperBound * (rank / buckets [0 ].count )
254
+ }
255
+
256
+ // linear approximation of b-th bucket
257
+ brank := rank - buckets [b - 1 ].count
258
+ bSize := buckets [b ].upperBound - buckets [b - 1 ].upperBound
259
+ bCount := buckets [b ].count - buckets [b - 1 ].count
260
+
261
+ return buckets [b - 1 ].upperBound + bSize * (brank / bCount )
262
+ }
263
+
264
+ // Quantile computes q-th quantile of a cumulative histogram.
265
+ // It's expected the histogram is valid (by calling Validate)
266
+ func (hist * Histogram ) Quantile (q float64 ) float64 {
267
+ buckets := []bucket {}
268
+
269
+ for _ , bckt := range hist .Bucket {
270
+ buckets = append (buckets , bucket {
271
+ count : float64 (* bckt .CumulativeCount ),
272
+ upperBound : * bckt .UpperBound ,
273
+ })
274
+ }
275
+
276
+ // bucketQuantile expects the upper bound of the last bucket to be +inf
277
+ // buckets[len(buckets)-1].upperBound = math.Inf(+1)
278
+
279
+ return bucketQuantile (q , buckets )
280
+ }
281
+
282
+ // Average computes histogram's average value
283
+ func (hist * Histogram ) Average () float64 {
284
+ return * hist .SampleSum / float64 (* hist .SampleCount )
285
+ }
286
+
287
+ // Clear clears all fields of the wrapped histogram
288
+ func (hist * Histogram ) Clear () {
289
+ if hist .SampleCount != nil {
290
+ * hist .SampleCount = 0
291
+ }
292
+ if hist .SampleSum != nil {
293
+ * hist .SampleSum = 0
294
+ }
295
+ for _ , b := range hist .Bucket {
296
+ if b .CumulativeCount != nil {
297
+ * b .CumulativeCount = 0
298
+ }
299
+ }
300
+ }
301
+
302
+ // Validate makes sure the wrapped histogram has all necessary fields set and with valid values.
303
+ func (hist * Histogram ) Validate () error {
304
+ if hist .SampleCount == nil || * hist .SampleCount == 0 {
305
+ return fmt .Errorf ("nil or empty histogram SampleCount" )
306
+ }
307
+
308
+ if hist .SampleSum == nil || * hist .SampleSum == 0 {
309
+ return fmt .Errorf ("nil or empty histogram SampleSum" )
310
+ }
311
+
312
+ for _ , bckt := range hist .Bucket {
313
+ if bckt == nil {
314
+ return fmt .Errorf ("empty histogram bucket" )
315
+ }
316
+ if bckt .UpperBound == nil || * bckt .UpperBound < 0 {
317
+ return fmt .Errorf ("nil or negative histogram bucket UpperBound" )
318
+ }
319
+ }
320
+
321
+ return nil
322
+ }
0 commit comments