model/histogram: Validate non-negative count and zero bucket

We have always validated that none of the bucket is negative. We
should do the same for the count of observations and the zero bucket.

Note that this was always implied in the protobuf exposition format
because a count or a zero bucket population is ignored if it is not
positive.

Signed-off-by: beorn7 <beorn@grafana.com>
This commit is contained in:
beorn7 2025-09-30 19:03:46 +02:00
parent cc7b1de372
commit 3d7cf4c274
4 changed files with 102 additions and 0 deletions

View File

@ -826,12 +826,18 @@ func (h *FloatHistogram) Validate() error {
if err != nil {
return fmt.Errorf("negative side: %w", err)
}
if h.ZeroCount < 0 {
return fmt.Errorf("zero bucket has observation count of %v: %w", h.ZeroCount, ErrHistogramNegativeBucketCount)
}
if h.CustomValues != nil {
return ErrHistogramExpSchemaCustomBounds
}
default:
return InvalidSchemaError(h.Schema)
}
if h.Count < 0 {
return fmt.Errorf("observation count is %v: %w", h.Count, ErrHistogramNegativeCount)
}
err := checkHistogramBuckets(h.PositiveBuckets, &pCount, false)
if err != nil {
return fmt.Errorf("positive side: %w", err)

View File

@ -3527,6 +3527,100 @@ func TestFloatHistogramString(t *testing.T) {
}
}
func TestFloatHistogramValidateNegativeHistogram(t *testing.T) {
cases := []struct {
name string
fh *FloatHistogram
}{
{
"positive bucket with negative population",
&FloatHistogram{
Schema: 1,
ZeroThreshold: 0.01,
ZeroCount: 5.5,
Count: 3493.3,
Sum: 2349209.324,
PositiveSpans: []Span{
{-2, 1},
{2, 3},
},
PositiveBuckets: []float64{1, 3.3, -4.2, 0.1},
NegativeSpans: []Span{
{3, 2},
{3, 2},
},
NegativeBuckets: []float64{3.1, 3, 1.234e5, 1000},
},
},
{
"negative bucket with negative population",
&FloatHistogram{
Schema: 1,
ZeroThreshold: 0.01,
ZeroCount: 5.5,
Count: 3493.3,
Sum: 2349209.324,
PositiveSpans: []Span{
{-2, 1},
{2, 3},
},
PositiveBuckets: []float64{1, 3.3, 4.2, 0.1},
NegativeSpans: []Span{
{3, 2},
{3, 2},
},
NegativeBuckets: []float64{3.1, -3, 1.234e5, 1000},
},
},
{
"negative count",
&FloatHistogram{
Schema: 1,
ZeroThreshold: 0.01,
ZeroCount: 5.5,
Count: -3493.3,
Sum: 2349209.324,
PositiveSpans: []Span{
{-2, 1},
{2, 3},
},
PositiveBuckets: []float64{1, 3.3, 4.2, 0.1},
NegativeSpans: []Span{
{3, 2},
{3, 2},
},
NegativeBuckets: []float64{3.1, 3, 1.234e5, 1000},
},
},
{
"zero bucket with negative population",
&FloatHistogram{
Schema: 1,
ZeroThreshold: 0.01,
ZeroCount: -5.5,
Count: 3493.3,
Sum: 2349209.324,
PositiveSpans: []Span{
{-2, 1},
{2, 3},
},
PositiveBuckets: []float64{1, 3.3, 4.2, 0.1},
NegativeSpans: []Span{
{3, 2},
{3, 2},
},
NegativeBuckets: []float64{3.1, 3, 1.234e5, 1000},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
require.Error(t, c.fh.Validate())
})
}
}
func BenchmarkFloatHistogramAllBucketIterator(b *testing.B) {
rng := rand.New(rand.NewSource(0))

View File

@ -31,6 +31,7 @@ const (
var (
ErrHistogramCountNotBigEnough = errors.New("histogram's observation count should be at least the number of observations found in the buckets")
ErrHistogramCountMismatch = errors.New("histogram's observation count should equal the number of observations found in the buckets (in absence of NaN)")
ErrHistogramNegativeCount = errors.New("histogram's observation count is negative")
ErrHistogramNegativeBucketCount = errors.New("histogram has a bucket whose observation count is negative")
ErrHistogramSpanNegativeOffset = errors.New("histogram has a span whose offset is negative")
ErrHistogramSpansBucketsMismatch = errors.New("histogram spans specify different number of buckets than provided")

View File

@ -122,6 +122,7 @@ func isHistogramValidationError(err error) bool {
// TODO: Consider adding single histogram error type instead of individual sentinel errors.
return errors.Is(err, histogram.ErrHistogramCountMismatch) ||
errors.Is(err, histogram.ErrHistogramCountNotBigEnough) ||
errors.Is(err, histogram.ErrHistogramNegativeCount) ||
errors.Is(err, histogram.ErrHistogramNegativeBucketCount) ||
errors.Is(err, histogram.ErrHistogramSpanNegativeOffset) ||
errors.Is(err, histogram.ErrHistogramSpansBucketsMismatch) ||