Skip to content

Commit 90b169a

Browse files
authored
Merge pull request #1314 from prometheus/beorn7/histogram
histogram: Enable detection of a native histogram without observations
2 parents 98025d8 + 3d82c94 commit 90b169a

File tree

2 files changed

+53
-24
lines changed

2 files changed

+53
-24
lines changed

prometheus/histogram.go

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -428,12 +428,12 @@ type HistogramOpts struct {
428428
// a major version bump.
429429
NativeHistogramBucketFactor float64
430430
// All observations with an absolute value of less or equal
431-
// NativeHistogramZeroThreshold are accumulated into a “zero”
432-
// bucket. For best results, this should be close to a bucket
433-
// boundary. This is usually the case if picking a power of two. If
431+
// NativeHistogramZeroThreshold are accumulated into a “zero” bucket.
432+
// For best results, this should be close to a bucket boundary. This is
433+
// usually the case if picking a power of two. If
434434
// NativeHistogramZeroThreshold is left at zero,
435-
// DefNativeHistogramZeroThreshold is used as the threshold. To configure
436-
// a zero bucket with an actual threshold of zero (i.e. only
435+
// DefNativeHistogramZeroThreshold is used as the threshold. To
436+
// configure a zero bucket with an actual threshold of zero (i.e. only
437437
// observations of precisely zero will go into the zero bucket), set
438438
// NativeHistogramZeroThreshold to the NativeHistogramZeroThresholdZero
439439
// constant (or any negative float value).
@@ -446,23 +446,28 @@ type HistogramOpts struct {
446446
// Histogram are sufficiently wide-spread. In particular, this could be
447447
// used as a DoS attack vector. Where the observed values depend on
448448
// external inputs, it is highly recommended to set a
449-
// NativeHistogramMaxBucketNumber.) Once the set
449+
// NativeHistogramMaxBucketNumber.) Once the set
450450
// NativeHistogramMaxBucketNumber is exceeded, the following strategy is
451-
// enacted: First, if the last reset (or the creation) of the histogram
452-
// is at least NativeHistogramMinResetDuration ago, then the whole
453-
// histogram is reset to its initial state (including regular
454-
// buckets). If less time has passed, or if
455-
// NativeHistogramMinResetDuration is zero, no reset is
456-
// performed. Instead, the zero threshold is increased sufficiently to
457-
// reduce the number of buckets to or below
458-
// NativeHistogramMaxBucketNumber, but not to more than
459-
// NativeHistogramMaxZeroThreshold. Thus, if
460-
// NativeHistogramMaxZeroThreshold is already at or below the current
461-
// zero threshold, nothing happens at this step. After that, if the
462-
// number of buckets still exceeds NativeHistogramMaxBucketNumber, the
463-
// resolution of the histogram is reduced by doubling the width of the
464-
// sparse buckets (up to a growth factor between one bucket to the next
465-
// of 2^(2^4) = 65536, see above).
451+
// enacted:
452+
// - First, if the last reset (or the creation) of the histogram is at
453+
// least NativeHistogramMinResetDuration ago, then the whole
454+
// histogram is reset to its initial state (including regular
455+
// buckets).
456+
// - If less time has passed, or if NativeHistogramMinResetDuration is
457+
// zero, no reset is performed. Instead, the zero threshold is
458+
// increased sufficiently to reduce the number of buckets to or below
459+
// NativeHistogramMaxBucketNumber, but not to more than
460+
// NativeHistogramMaxZeroThreshold. Thus, if
461+
// NativeHistogramMaxZeroThreshold is already at or below the current
462+
// zero threshold, nothing happens at this step.
463+
// - After that, if the number of buckets still exceeds
464+
// NativeHistogramMaxBucketNumber, the resolution of the histogram is
465+
// reduced by doubling the width of the sparse buckets (up to a
466+
// growth factor between one bucket to the next of 2^(2^4) = 65536,
467+
// see above).
468+
// - Any increased zero threshold or reduced resolution is reset back
469+
// to their original values once NativeHistogramMinResetDuration has
470+
// passed (since the last reset or the creation of the histogram).
466471
NativeHistogramMaxBucketNumber uint32
467472
NativeHistogramMinResetDuration time.Duration
468473
NativeHistogramMaxZeroThreshold float64
@@ -782,6 +787,16 @@ func (h *histogram) Write(out *dto.Metric) error {
782787
his.ZeroCount = proto.Uint64(zeroBucket)
783788
his.NegativeSpan, his.NegativeDelta = makeBuckets(&coldCounts.nativeHistogramBucketsNegative)
784789
his.PositiveSpan, his.PositiveDelta = makeBuckets(&coldCounts.nativeHistogramBucketsPositive)
790+
791+
// Add a no-op span to a histogram without observations and with
792+
// a zero threshold of zero. Otherwise, a native histogram would
793+
// look like a classic histogram to scrapers.
794+
if *his.ZeroThreshold == 0 && *his.ZeroCount == 0 && len(his.PositiveSpan) == 0 && len(his.NegativeSpan) == 0 {
795+
his.PositiveSpan = []*dto.BucketSpan{{
796+
Offset: proto.Int32(0),
797+
Length: proto.Uint32(0),
798+
}}
799+
}
785800
}
786801
addAndResetCounts(hotCounts, coldCounts)
787802
return nil
@@ -854,13 +869,16 @@ func (h *histogram) limitBuckets(counts *histogramCounts, value float64, bucket
854869
h.doubleBucketWidth(hotCounts, coldCounts)
855870
}
856871

857-
// maybeReset resests the whole histogram if at least h.nativeHistogramMinResetDuration
872+
// maybeReset resets the whole histogram if at least h.nativeHistogramMinResetDuration
858873
// has been passed. It returns true if the histogram has been reset. The caller
859874
// must have locked h.mtx.
860-
func (h *histogram) maybeReset(hot, cold *histogramCounts, coldIdx uint64, value float64, bucket int) bool {
875+
func (h *histogram) maybeReset(
876+
hot, cold *histogramCounts, coldIdx uint64, value float64, bucket int,
877+
) bool {
861878
// We are using the possibly mocked h.now() rather than
862879
// time.Since(h.lastResetTime) to enable testing.
863-
if h.nativeHistogramMinResetDuration == 0 || h.now().Sub(h.lastResetTime) < h.nativeHistogramMinResetDuration {
880+
if h.nativeHistogramMinResetDuration == 0 ||
881+
h.now().Sub(h.lastResetTime) < h.nativeHistogramMinResetDuration {
864882
return false
865883
}
866884
// Completely reset coldCounts.

prometheus/histogram_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,17 @@ func TestNativeHistogram(t *testing.T) {
485485
factor: 1,
486486
want: `sample_count:3 sample_sum:6 bucket:<cumulative_count:0 upper_bound:0.005 > bucket:<cumulative_count:0 upper_bound:0.01 > bucket:<cumulative_count:0 upper_bound:0.025 > bucket:<cumulative_count:0 upper_bound:0.05 > bucket:<cumulative_count:0 upper_bound:0.1 > bucket:<cumulative_count:0 upper_bound:0.25 > bucket:<cumulative_count:0 upper_bound:0.5 > bucket:<cumulative_count:1 upper_bound:1 > bucket:<cumulative_count:2 upper_bound:2.5 > bucket:<cumulative_count:3 upper_bound:5 > bucket:<cumulative_count:3 upper_bound:10 > `, // Has conventional buckets because there are no sparse buckets.
487487
},
488+
{
489+
name: "no observations",
490+
factor: 1.1,
491+
want: `sample_count:0 sample_sum:0 schema:3 zero_threshold:2.938735877055719e-39 zero_count:0 `,
492+
},
493+
{
494+
name: "no observations and zero threshold of zero resulting in no-op span",
495+
factor: 1.1,
496+
zeroThreshold: NativeHistogramZeroThresholdZero,
497+
want: `sample_count:0 sample_sum:0 schema:3 zero_threshold:0 zero_count:0 positive_span:<offset:0 length:0 > `,
498+
},
488499
{
489500
name: "factor 1.1 results in schema 3",
490501
observations: []float64{0, 1, 2, 3},

0 commit comments

Comments
 (0)