Skip to content

Reject histogram buckets with duplicate le label float values - #346

Open
sahilleth wants to merge 1 commit into
GoogleCloudPlatform:release-2.53.5-gmpfrom
sahilleth:fix-982-histogram-duplicate-buckets
Open

Reject histogram buckets with duplicate le label float values#346
sahilleth wants to merge 1 commit into
GoogleCloudPlatform:release-2.53.5-gmpfrom
sahilleth:fix-982-histogram-duplicate-buckets

Conversation

@sahilleth

Copy link
Copy Markdown

Summary

  • Reject histogram bucket boundaries when different le label strings parse to the same float64 (e.g. "0.005" and "0.00500000000000000006), not only when bounds are strictly decreasing
  • Detect duplicate boundaries early when buckets are appended during distribution assembly
  • Improve error messages to include the histogram metric name and conflicting le label values

Fixes GoogleCloudPlatform/prometheus-engine#982

Test plan

  • go test ./google/export/ -count=1
  • New regression test for 0.005 / 0.00500000000000000006 duplicate labels

Duplicate le strings that parse to the same float64 (e.g. 0.005 and
0.00500000000000000006) produce invalid GCM distributions. Detect them
at parse time and during build with clearer errors that include the
metric name and le labels.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds validation to detect and reject duplicate bucket boundaries in Prometheus histograms, specifically addressing cases where different 'le' label strings parse to the same float64 value. It tracks the original 'le' labels and provides descriptive error messages. The review feedback highlights two critical issues: first, returning an error immediately when a duplicate is found during sample processing causes a stream desynchronization bug by leaving remaining samples of the same histogram family unconsumed; second, 'NaN' boundaries can bypass duplicate checks and violate ordering requirements, so they should be explicitly rejected early.

Comment on lines +502 to +512
for i, existing := range dist.bounds {
if existing == bound {
prometheusSamplesDiscarded.WithLabelValues("duplicate-bucket-boundary").Add(float64(dist.inputSampleCount()))
discardExemplarIncIfExists(storage.SeriesRef(s.Ref), exemplars, "duplicate-bucket-boundary")
lePrev := ""
if len(dist.leLabels) == len(dist.bounds) {
lePrev = dist.leLabels[i]
}
return nil, samples[consumed:], duplicateBucketBoundaryError(metric, lePrev, leLabel, bound, e.lset)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Returning an error immediately inside the loop in buildDistributions causes a stream desynchronization bug. Any remaining samples belonging to the same histogram family will not be consumed, leaving them in the returned samples[consumed:] slice. If the caller continues processing the remainder, it will attempt to process those remaining samples as a new, incomplete histogram, leading to corrupt metrics or further errors.

To fix this, we should consume all remaining samples of the same histogram family before returning the error.

            for i, existing := range dist.bounds {
                if existing == bound {
                    prometheusSamplesDiscarded.WithLabelValues("duplicate-bucket-boundary").Add(float64(dist.inputSampleCount()))
                    discardExemplarIncIfExists(storage.SeriesRef(s.Ref), exemplars, "duplicate-bucket-boundary")
                    lePrev := ""
                    if len(dist.leLabels) == len(dist.bounds) {
                        lePrev = dist.leLabels[i]
                    }
                    for _, remainingSample := range samples[consumed:] {
                        eRem, ok := b.series.get(remainingSample, externalLabels, metadata)
                        if !ok {
                            consumed++
                            continue
                        }
                        nameRem := eRem.lset.Get(labels.MetricName)
                        if !isHistogramSeries(metric, nameRem) {
                            break
                        }
                        consumed++
                    }
                    return nil, samples[consumed:], duplicateBucketBoundaryError(metric, lePrev, leLabel, bound, e.lset)
                }
            }

Comment on lines +491 to 492
bound, err := strconv.ParseFloat(leLabel, 64)
if err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If a metric has a bucket label le="NaN", strconv.ParseFloat will successfully parse it as math.NaN(). This NaN boundary will bypass the duplicate checks (since NaN == NaN is false) and will be appended to dist.bounds. This violates the strict weak ordering required by sort.Sort and will eventually cause the Google Cloud Monitoring API to reject the entire write request.

We should explicitly reject NaN boundaries early as malformed bucket labels.

Suggested change
bound, err := strconv.ParseFloat(leLabel, 64)
if err != nil {
bound, err := strconv.ParseFloat(leLabel, 64)
if err != nil || math.IsNaN(bound) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve error handling when translating classic histograms to distribution (e.g. parsing bucket "le" label edge cases)

1 participant