Skip to content

promql: Implement </ and >/ operators for trimming native histograms.#16330

Closed
sujalshah-bit wants to merge 2 commits into
prometheus:mainfrom
sujalshah-bit:trim_histogram
Closed

promql: Implement </ and >/ operators for trimming native histograms.#16330
sujalshah-bit wants to merge 2 commits into
prometheus:mainfrom
sujalshah-bit:trim_histogram

Conversation

@sujalshah-bit

@sujalshah-bit sujalshah-bit commented Mar 26, 2025

Copy link
Copy Markdown
Contributor

Implement </ and >/ operators for trimming native histograms

This PR implements the </ and >/ operators for trimming observations from native histograms.

  • </ (TRIM_UPPER): Removes all observations above a threshold value
  • >/ (TRIM_LOWER): Removes all observations below a threshold value

Implementation Details

The implementation adds support for these operations by:

  1. Adding new operator types (TRIM_UPPER and TRIM_LOWER) to the parser
  2. Implementing a trimHistogram helper function to zero out buckets that should be excluded
  3. Extending vectorElemBinop to handle these new operations

Usage Examples

# Remove all observations above 0.1 from the histogram
request_duration_seconds </ 0.1

# Remove all observations below 1 from the histogram
request_duration_seconds >/ 1

Postive histogram

request_duration_seconds.mp4

Negative histogram

negative_value_histogram.mp4

Mixed value histogram

mixed_value_histogram.mp4

Important Notes on Behavior

The trimming operators work by zeroing out buckets that are fully outside the desired range. When the threshold falls within a bucket, the entire bucket is preserved or removed based on the comparison with the bucket boundary.

Limitations

It's important to note that while these operators can trim histograms at specific thresholds, they cannot be combined with the standard comparison operators (<, >, <=, >=) as those do not currently support histogram-to-float comparisons. Attempting operations like:

(request_duration_seconds >/ 0.1) > 0.1

Will result in an error: incompatible sample types encountered for binary operator "<": histogram < float

Future Considerations

As discussed in Comment by @beorn7 in #14651, if comparison operators between histograms and floats are implemented in the future, there could be potential semantic inconsistencies between the behavior of trimming and comparison operations, particularly when thresholds don't align with bucket boundaries.

For example, if both operators were supported, a query like (histogram </ 0.15) < 0.15 might not behave as intuitively expected due to how histogram buckets and interpolation work.

Fixes #14651.

@sujalshah-bit
sujalshah-bit force-pushed the trim_histogram branch 4 times, most recently from dd12362 to e3db1b9 Compare April 2, 2025 12:04
@sujalshah-bit sujalshah-bit changed the title WIP promql: Implement </ and >/ operators for trimming native histograms. Apr 2, 2025
@sujalshah-bit
sujalshah-bit marked this pull request as ready for review April 2, 2025 12:32
@juliusv
juliusv requested a review from beorn7 April 2, 2025 13:30
@juliusv

juliusv commented Apr 2, 2025

Copy link
Copy Markdown
Member

Before leaving the real review to @beorn7 the native histogram expert, a few quick observations / questions:

  • Seems like the autocomplete for the new operator is still missing?
  • Similar point for linting adjustments in the editor: an expression like foo </ bool 1 currently gives no linter error but fails with an error like bool modifier can only be used on comparison operators. An expression like 1 </ 1 gives the linter error comparisons between scalars must use BOOL modifier, but the bool modifier does not help / is not legal in this case. I guess there needs to be just more differentiation between the regular comparison operators and the native histogram trimming ones.
  • Should the count and sum fields stay completely untouched as they currently are? I guess you can't really change sum in a good way, and thus you also keep count without removing the filtered elements from it? It's just a bit funky to get a derived histogram that has potentially no buckets at all anymore, but a big count and sum. Not sure what to do about that though (unless we also stored a sum per bucket or estimated it by doing count * (lower_bound + upper_bound)/2).
  • If I try running non_histogram_metric </ 1, I get the error operator "</" not allowed for operations between Vectors. Maybe the error message should be more fitting here, saying something about the operator only working on native histograms and scalars?

@sujalshah-bit
sujalshah-bit force-pushed the trim_histogram branch 3 times, most recently from 24a997a to 75abf11 Compare April 5, 2025 01:14
@sujalshah-bit

sujalshah-bit commented Apr 5, 2025

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback, @juliusv! I've made the following changes:

  • Autocomplete support for the new operator (</, >/) has been added.

  • ✅ The linter now properly distinguishes native histogram trimming operators from standard comparison operators:

    • foo </ bool 1 now triggers linting error:
      "bool modifier can only be used on comparison operators"
    • 1 </ 1 now triggers linting error:
      "operator "</" not allowed for Scalar operations"
  • ✅ I’ve updated how the sum is handled for trimmed histograms:

    updatedCount += bucket.Count
    bucketMidpoint := (bucket.Lower + bucket.Upper) / 2
    updatedSum += bucket.Count * bucketMidpoint

    This way, sum is now roughly estimated per bucket, and count reflects only the buckets that remain after trimming.

  • ✅ The error message for invalid usage like non_histogram_metric </ 1 now correctly states:
    "operator "</" is only supported for native histograms and scalars, not standard vector operations"

Let me know if you have any further suggestions!

@sujalshah-bit

Copy link
Copy Markdown
Contributor Author

@juliusv is it ready for review for @beorn7 ?

@beorn7

beorn7 commented Apr 8, 2025

Copy link
Copy Markdown
Contributor

I'll have a look at this ASAP. Also, @NeerajGartia21 might help with the reviewing. He has worked a lot on histogram functions recently and has become one of the domain experts.

@NeerajGartia21

Copy link
Copy Markdown
Contributor

Thanks for the PR, @sujalshah-bit! I’ll review it in detail in 1–2 days. In the meantime, please add tests for the engine, lexer, and parser to ensure everything is working correctly. You can refer to this for adding tests to the PromQL engine.

@NeerajGartia21 NeerajGartia21 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.

The parser-side changes look good, but I think the trimming logic needs improvement:

  1. There's a mismatch between the operator name and its behavior. In #14651 (comment) , @beorn7 mentioned that histogram_metric </ 0.1 should remove observations greater than 0.1. This means it trims the right part of the histogram, so logically it should be called RTRIM. However, the PR description currently says that </ removes observations below the threshold, which contradicts both the discussion and the current implementation. Would be good to clarify this, but I’ll leave the final word to @beorn7.

  2. Right now, when the trim value lies between a bucket’s bounds, the entire bucket is discarded. But ideally, we should estimate (via interpolation) how many observations to remove from that bucket.
    The current logic only works correctly when the bucket boundary exactly matches the trim parameter.
    For example, if the trim value is 0.8, schema is 0, and all observations lie in [0.5, 1), applying LTRIM currently gives no observations, but we should actually retain some.

    Also, make sure this works with native histograms with custom buckets, and handle zero buckets correctly as well.

For reference, you can look at the interpolation logic in the histogram_quantile and histogram_fraction functions here.

Comment thread promql/engine.go Outdated
return 0, hlhs.Copy().Div(rhs).Compact(0), true, nil
case parser.LTRIM:
trimmedHist := *hlhs
trimBuckets(&trimmedHist, hlhs, rhs, "LTRIM")

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.

Instead of passing the histogram and copying the buckets inside trimBuckets, we can directly pass a copy using hlhs.Copy(). This makes the code clearer. The same can be done for RTRIM as well.

Comment thread promql/engine.go Outdated
}

// Helper function to trim native histogram buckets.
func trimBuckets(trimmedHist, hlhs *histogram.FloatHistogram, rhs float64, mode string) {

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.

Consider renaming the trimBuckets function to trimHistogram for better consistency with the LTRIM and RTRIM operators. Additionally, since the function always operates on a copy of the original histogram, we can simplify the signature by removing the second hlhs argument and directly using the copied histogram inside the function.

Comment thread promql/engine.go Outdated

for i, iter := 0, hlhs.PositiveBucketIterator(); iter.Next(); i++ {
bucket := iter.At()
if (mode == "LTRIM" && bucket.Upper > rhs) || (mode == "RTRIM" && bucket.Lower < rhs) {

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.

This trimming logic might not handle all cases properly. Please check my main comment above for details. In short, fully removing a bucket just because the trim value falls inside it can be inaccurate. Using interpolation to estimate and remove only part of the bucket would give better results.

Comment thread promql/engine.go Outdated
}
}
if parser.LTRIM == op || parser.RTRIM == op {
panic(fmt.Errorf("operator %q is only supported for native histograms and scalars, not standard vector operations", op))

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.

We probably shouldn't panic in these cases. There could be situations where this operation is done between a histogram and a vector that has a mix of floats and histograms. In such cases, the user might expect partial results instead of a full failure. It would be better to return an info annotation here rather than panicking.

Comment thread promql/engine.go Outdated
case parser.LTRIM:
trimmedHist := *hlhs
trimBuckets(&trimmedHist, hlhs, rhs, "LTRIM")
return 0, &trimmedHist, true, 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.

We should also consider how this behaves when the bool modifier is used.
@beorn7, can you please clarify what the expected output should be in this case?

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.

I don't think the </ and >/ operators should be allowed with the bool modifier. It's a syntax error.

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.

Yeah, so we should disable it. Currently, it does not throw any syntax error.

Comment thread promql/engine.go Outdated
}
}

for i, iter := 0, hlhs.NegativeBucketIterator(); iter.Next(); i++ {

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.

Instead of iterating through positive and negative buckets separately, we could simplify the code by using AllBucketIterator to iterate over both at once. This might make the logic cleaner and easier to maintain.

@juliusv

juliusv commented Apr 10, 2025

Copy link
Copy Markdown
Member
  1. There's a mismatch between the operator name and its behavior. In native histograms: Implement the </ and >/ operators to "trim" a histogram. #14651 (comment) , @beorn7 mentioned that histogram_metric </ 0.1 should remove observations greater than 0.1. This means it trims the right part of the histogram, so logically it should be called RTRIM. However, the PR description currently says that </ removes observations below the threshold, which contradicts both the discussion and the current implementation. Would be good to clarify this, but I’ll leave the final word to @beorn7.

While I can logically understand either variant, the second one feels way more intuitive to me when taking into account that traditional filter operators like > and < keep (and not drop) those series of a vector that match the filter condition. So I would expect histogram_metric </ 0.1 to keep only the observations less than 0.1 and inverting it would be confusing.

@beorn7

beorn7 commented Apr 10, 2025

Copy link
Copy Markdown
Contributor

Thanks for the great discussions. I'll try to answer all the questions ASAP. (Still swamped with things after KubeCon.)

@NeerajGartia21

Copy link
Copy Markdown
Contributor

While I can logically understand either variant, the second one feels way more intuitive to me when taking into account that traditional filter operators like > and < keep (and not drop) those series of a vector that match the filter condition. So I would expect histogram_metric </ 0.1 to keep only the observations less than 0.1 and inverting it would be confusing.

Makes sense. We could also call them trimUpper and trimLower.

  • </ as trimUpper (removes values > threshold)
  • >/ as trimLower (removes values < threshold)

Might make things clearer.

@juliusv

juliusv commented Apr 10, 2025

Copy link
Copy Markdown
Member

While I can logically understand either variant, the second one feels way more intuitive to me when taking into account that traditional filter operators like > and < keep (and not drop) those series of a vector that match the filter condition. So I would expect histogram_metric </ 0.1 to keep only the observations less than 0.1 and inverting it would be confusing.

Makes sense. We could also call them trimUpper and trimLower.

  • </ as trimUpper (removes values > threshold)
  • >/ as trimLower (removes values < threshold)

Yeah, I think that kind of naming would make sense, it would mirror what e.g. Go does with TrimLeft() and TrimRight() (naming the part that is removed).

@sujalshah-bit

Copy link
Copy Markdown
Contributor Author

Just to confirm my understanding:

The intended behavior is that histogram_metric </ rhs keeps only values less than rhs (i.e., trims away values ≥ rhs), and >/ rhs keeps only values greater than rhs (i.e., trims away values ≤ rhs).
So the PR description needs to be corrected to match this behavior — is that right?

@juliusv

juliusv commented Apr 14, 2025

Copy link
Copy Markdown
Member

Just to confirm my understanding:

The intended behavior is that histogram_metric </ rhs keeps only values less than rhs (i.e., trims away values ≥ rhs), and >/ rhs keeps only values greater than rhs (i.e., trims away values ≤ rhs). So the PR description needs to be corrected to match this behavior — is that right?

Correct!

@beorn7

beorn7 commented Apr 16, 2025

Copy link
Copy Markdown
Contributor

High level answers (I have not yet looked at the code):

WRT direction: I think the discussion above came to the right conclusion: histogram </ threshold means to filter out observation ≥ threshold and keep the observations < threshold (AKA TrimUpper), and vice versa for >/.

WRT interpolation: I believe we should do the interpolation. Linear interpolation for NHCB and in the zero bucket, and exponential interpolation for the standard exponential schemas. As @NeerajGartia21 said:

For reference, you can look at the interpolation logic in the histogram_quantile and histogram_fraction functions here.

WRT count and sum: Those need to be adjusted as well, as @sujalshah-bit already discussed. To add more detail to the provided formula:

  • For count, we need to deduct the bucket count of entirely removed buckets. For the bucket into which we have interpolated, we need to deduct the count we have removed from that bucket as per the interpolation.
  • For sum, we can indeed use the midpoint of each bucket for linear interpolation (i.e. for NHCB and the zero bucket). For the exponential bucket, we should use the geometric mean (cf. how it is done for histogram_stddev). For the bucket that we are only partially removing, we need to take the mean between the relevant remaining bucket boundary and the threshold. (I can flesh this out with an example if it isn't clear.)

@beorn7

beorn7 commented Apr 16, 2025

Copy link
Copy Markdown
Contributor

@sujalshah-bit do you feel with the collected comments that you can go into the next iteration of working on the code?

My impression is that a code-level review only makes sense once you have worked in the high level notes above. If you need some code-level assistance, please let me know. (Note that @NeerajGartia21 already gave valuable code level comments.)

Also, of course, feel free to ask any questions you might still have to fully understand what we want to implement here.

@beorn7

beorn7 commented Apr 16, 2025

Copy link
Copy Markdown
Contributor

(Side note: When mentioning the geometric mean for histogram_stddev above, I realized that this function should take the arithmetic mean for NHCB. I filed #16439 for it.)

@sujalshah-bit

Copy link
Copy Markdown
Contributor Author

Thanks a lot for the detailed discussion above — I’ve learned a lot, and will be updating the changes soon!

I'm trying to generate a customized bucket for native histograms for testing this feature, but it's not working as expected.

By expected I mean:

CustomValues []float64 // this is empty and,
FloatHistogram.UsesCustomBuckets() // this is returning false

Here's the code I'm using to define the histogram:

customBuckets = promauto.NewHistogram(prometheus.HistogramOpts{
	Name: "custom_bucketss",
	Help: "Histogram with manually defined custom buckets",
	Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
	NativeHistogramBucketFactor: 1.1,
})
rng = rand.New(rand.NewPCG(uint64(time.Now().UnixNano()), 1))
customValue := rng.Float64() * 5.0
customBuckets.Observe(customValue)

Am I missing anything required to make it a proper NHCB (Native Histogram with Custom Buckets) for testing purposes?

Comment on lines +1325 to +1328
h_test {{schema:0 count:34 z_bucket:1 z_bucket_w:0.001 buckets:[2 4 8 16] n_buckets:[1 2]}}
h_test_2 {{schema:2 count:27 z_bucket:1 z_bucket_w:0.001 buckets:[1 2 4 7 3] n_buckets:[1 5 3 1]}}
cbh {{schema:-53 sum:5 count:15 custom_values:[5 10 15 20] buckets:[1 6 4 3 1]}}
zero_bucket {{schema:0 z_bucket:5 z_bucket_w:0.01 buckets:[2 3] n_buckets:[1 2 3]}}

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.

To elaborate on my comment:

These should all have sum:... fields with plausible values.

cbd already has a sum:... field, but its value is not plausible. If we assume that the observations are more or less in the middle of each bucket (and around 2.5 for the first bucket and not much above 20 for the +Inf bucket), the sum should be around 170.

@sujalshah-bit
sujalshah-bit force-pushed the trim_histogram branch 2 times, most recently from 2b339fe to d557626 Compare June 28, 2025 17:21

@krajorama krajorama left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

huge work, thanks for taking this on! I'm late to the party, but adding some comments.

eval instant at 1m h_test_2 </ -1.3
{__name__="h_test_2"} {{schema:2 count:2.45786052095524 sum:-16.03281816946792 z_bucket_w:0.001 n_offset:2 n_buckets:[1.45786052095524 1]}}

# Native Histogram: Linear Bucket Trimming Tests

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# Native Histogram: Linear Bucket Trimming Tests
# Exponential buckets: trim on bucket boundary uses no interpolation

cbh {{schema:-53 sum:172.5 count:15 custom_values:[5 10 15 20] buckets:[1 6 4 3 1]}}
zero_bucket {{schema:0 sum:-6.75 z_bucket:5 z_bucket_w:0.01 buckets:[2 3] n_buckets:[1 2 3]}}

# Native Histogram: Exponential Bucket Interpolation Tests

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# Native Histogram: Exponential Bucket Interpolation Tests
# Exponential buckets: trim uses exponential interpolation if cutoff is inside a bucket

eval instant at 1m h_test </ -1
{__name__="h_test"} {{count:2 sum:2.834740417100363 z_bucket_w:0.001 n_offset:1 n_buckets:[2]}}

# Custom Buckets: Trim Operation Tests

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# Custom Buckets: Trim Operation Tests
# Custom buckets: trim on bucket boundary without interpolation

Comment thread promql/promqltest/testdata/native_histograms.test
Comment on lines +1387 to +1388
eval instant at 1m h_test_2 </ 1.13
{__name__="h_test_2"} {{schema:2 count:13.410582181123704 sum:-9.282809901015558 z_bucket:1 z_bucket_w:0.001 buckets:[1 1.410582181123704] n_buckets:[1 5 3 1]}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For all test cases.

No need to use __name__ label, you can just write the name of the metric as h_test_2{}.
Also please add some indent on the result lines.

Suggested change
eval instant at 1m h_test_2 </ 1.13
{__name__="h_test_2"} {{schema:2 count:13.410582181123704 sum:-9.282809901015558 z_bucket:1 z_bucket_w:0.001 buckets:[1 1.410582181123704] n_buckets:[1 5 3 1]}}
eval instant at 1m h_test_2 </ 1.13
h_test_2{} {{schema:2 count:13.410582181123704 sum:-9.282809901015558 z_bucket:1 z_bucket_w:0.001 buckets:[1 1.410582181123704] n_buckets:[1 5 3 1]}}

Comment thread promql/engine.go Outdated
Comment on lines +3015 to +3016
var keepCount, bucketMidpoint float64
keepCount, bucketMidpoint = computeBucketTrim(op, bucket, rhs, true, isCustomBucket)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
var keepCount, bucketMidpoint float64
keepCount, bucketMidpoint = computeBucketTrim(op, bucket, rhs, true, isCustomBucket)
keepCount, bucketMidpoint := computeBucketTrim(op, bucket, rhs, true, isCustomBucket)

Comment thread promql/engine.go Outdated
Comment on lines +3043 to +3044
var keepCount, bucketMidpoint float64
keepCount, bucketMidpoint = computeBucketTrim(op, bucket, rhs, false, isCustomBucket)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
var keepCount, bucketMidpoint float64
keepCount, bucketMidpoint = computeBucketTrim(op, bucket, rhs, false, isCustomBucket)
keepCount, bucketMidpoint := computeBucketTrim(op, bucket, rhs, false, isCustomBucket)

Comment thread promql/engine.go Outdated
keepCount = bucket.Count
} else {
// Trim point is within bucket or below, keep none
keepCount = 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We definitely need at least a test case for this.

I can imagine two ways of handling this:

  • just do the bucket.Count*0.5 as for +Inf.
  • do bucket.Count*0.5 if the bucket.Upper is <= 0.0 otherwise assume this histogram is for positive values only (which we assume in histogram_quantile as well) and treat bucket.Lower as 0, which means if rhs < 0 then keepCount == 0 otherwise 0 <= rhs < bucket.Upper so we can do linear interpolation.

Comment thread promql/promqltest/testdata/native_histograms.test
Comment thread promql/promqltest/testdata/native_histograms.test
@sujalshah-bit
sujalshah-bit force-pushed the trim_histogram branch 2 times, most recently from 40a5452 to 48987c0 Compare July 7, 2025 12:33
@sujalshah-bit

Copy link
Copy Markdown
Contributor Author

@krajorama can you kindly review :)

@krajorama krajorama left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this is close to being merge-able. And maybe we should merge it to get a good baseline. I made one comment about TRIM_LOWER vs +Inf bucket which I think we should fix before merging. And more comments about exponential buckets and general structure which we could push later. wdyt @beorn7 ?

Comment thread promql/engine.go Outdated
if rhs <= bucket.Lower {
keepCount = bucket.Count
} else {
keepCount = 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

isn't this the same case as line 2956, so h </ 50 where 50 is in the highest +Inf bucket?

Suggested change
keepCount = 0
keepCount = bucket.Count * 0.5

Comment thread promql/engine.go Outdated
func computeExponentialTrim(bucket histogram.Bucket[float64], rhs float64, isPositive bool, op parser.ItemType) (float64, float64) {
var fraction, bucketMidpoint, keepCount float64

logLower := math.Log2(math.Abs(bucket.Lower))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Technically exponential buckets can also have -Inf and +Inf lower and upper bounds. See
https://prometheus.io/docs/specs/native_histograms/#buckets
and

func getBoundExponential(idx, schema int32) float64 {

Comment thread promql/engine.go
}

// Helper function to trim native histogram buckets.
func trimHistogram(trimmedHist *histogram.FloatHistogram, rhs float64, op parser.ItemType) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This function and the compute*Trim functions got a little bit complicated. Also as I note later, the computeExponentialTrim function is missing handling of overflow -Inf, +Inf buckets.

I'd like to suggest a simplification (we can discuss putting it in a follow-up PR)
let's have a function that has this signature:

computeSplit(bucket histogram.Bucket[float64], le float64, isPostive, isCustomBucket bool) (underCount, bucketMidPoint)

This would return the portion of the bucket count that's under or equal to the le and the midPoint of the bucket values (if possible).

Note that this doesn't have to take the Op. Since the result is directly usable as keepCount for TRIM_UPPER and you just need to subtract the underCount from the bucket count to get the keepCount value for TRIM_LOWER.

Also this would make the algorithm closer to

func HistogramFraction(lower, upper float64, h *histogram.FloatHistogram, metricName string, pos posrange.PositionRange) (float64, annotations.Annotations) {
which does something very similar.

Also also, the special handling for (-Inf, positive] bucket for custom buckets will never kick in for exponential histograms, since they don't have such bucket.

The resulting function should be possible to reuse here easily.

@beorn7

beorn7 commented Jul 8, 2025

Copy link
Copy Markdown
Contributor

Thanks, everyone. I'll give this a thorough review ASAP (hopefully tomorrow).

@sujalshah-bit

Copy link
Copy Markdown
Contributor Author

Should I go ahead and address this comment and this one in this PR, or should I work on those as a follow-up instead?

Either way, I’m happy to take care of it. Please let me know what works best.

Aside from those, I noticed only one other minor fix remaining (just have to push).

@beorn7

beorn7 commented Jul 10, 2025

Copy link
Copy Markdown
Contributor

Sorry for further delay. I'm still trying to do a thorough review this week, but there are other deadlines looming…

@sujalshah-bit in the meantime, please feel free to push the minor fix you were talking about above.

Also, on first glance, it seems useful to incorporate @krajorama's ideas about treating ±Inf edge cases, especially because things might actually get more straight forward after it. @sujalshah-bit your call. If you think it's a good idea, do it, but if you think it's better to work on it in another PR, that's also fine. (Modulo concerns I might find after my thorough review.)

This implements the TRIM_UPPER (</) and TRIM_LOWER (>/) operators
that allow removing observations below or above a threshold from
a histogram. The implementation zeros out buckets outside the desired
range. It also recalculates the sum, including only bucket counts within
the specified threshold range.

Fixes prometheus#14651.

Signed-off-by: sujal shah <[email protected]>
This implements the TRIM_UPPER (</) and TRIM_LOWER (>/) operators
that allow removing observations below or above a threshold from
a histogram. The implementation zeros out buckets outside the desired
range. It also recalculates the sum, including only bucket counts within
the specified threshold range.

Fixes prometheus#14651.

Signed-off-by: sujal shah <[email protected]>
@beorn7

beorn7 commented Jul 31, 2025

Copy link
Copy Markdown
Contributor

I was back in action for a few days, but now I'm going on vacations… @krajorama @sujalshah-bit if you find the time, please finish this without me. I'm taking a note that I'll check the logic of this in any case before we declare NH stable.

@sujalshah-bit

Copy link
Copy Markdown
Contributor Author

@krajorama I have made the changes you can have look.

@krajorama krajorama left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we're getting close. I have a bunch of nitpicks, a comment on the hasPositive/hasNegative calc and potentially an issue with the sum / midpoint calc.

Comment thread promql/engine.go
Comment on lines +2998 to +2999
hasPositive = true
bucket := iter.At()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Knowing if we have negative observations is a bit more complicated for custom bucket histograms. All buckets , even the ones with negative threshold are stored in the positive side. See in https://prometheus.io/docs/specs/native_histograms/#buckets. Also a bucket can have a lower bound < 0 and upper bound > 0.

The upshot is that we need to look at the actual Upper and Lower threshold to know what kind of bucket this is for custom histograms. For a bucket that falls bellow 0 , or completely above 0 ,it's easy. For buckets that straddle 0, I'd just be permissive and say it can have both values and not clamp. After all we use liner interpolation so errors cannot be that bad (I hope).

ALSO, as far as I see all calculations come out to 0 or no change if the bucket count is zero, so I'd do an early return, do you agree?

The upshot is that this line should be replaced with something like:

Suggested change
hasPositive = true
bucket := iter.At()
bucket := iter.At()
if bucket.Count == 0 {
continue
}
if isCustomBucket {
if bucket.Upper <= 0 {
hasNegative = true
} else {
if bucket.Lower < 0 {
hasNegative = true
}
hasPositive = true
}
} else {
hasPositive = true
}

Comment thread promql/engine.go
Comment on lines +3025 to +3026
hasNegative = true
bucket := iter.At()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can do this:

Suggested change
hasNegative = true
bucket := iter.At()
bucket := iter.At()
if bucket.Count == 0 {
continue
}
hasNegative = true

Comment thread promql/engine.go
Comment on lines +3049 to +3050
hasPositive = true
bucket := iter.At()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
hasPositive = true
bucket := iter.At()
bucket := iter.At()
if bucket.Count == 0 {
continue
}
if isCustomBucket {
if bucket.Upper <= 0 {
hasNegative = true
} else {
if bucket.Lower < 0 {
hasNegative = true
}
hasPositive = true
}
} else {
hasPositive = true
}

Comment thread promql/engine.go
Comment on lines +3073 to +3074
hasNegative = true
bucket := iter.At()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
hasNegative = true
bucket := iter.At()
bucket := iter.At()
if bucket.Count == 0 {
continue
}

Comment thread promql/engine.go
}

// computeSplit calculates the portion of the bucket's count <= le (trim point).
func computeSplit(b histogram.Bucket[float64], le float64, isPositive, isCustom bool) float64 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's rename le to rhs to be consistent. Easier to read.

Comment thread promql/engine.go
Comment on lines +3058 to +3059
removedMid := bucketMidpoint
removedSum += removedCount * removedMid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
removedMid := bucketMidpoint
removedSum += removedCount * removedMid
removedSum += removedCount * bucketMidpoint

Comment thread promql/engine.go
Comment on lines +3009 to +3010
removedMid := bucketMidpoint
removedSum += removedCount * removedMid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like removedMid isn't used anywhere else.

Suggested change
removedMid := bucketMidpoint
removedSum += removedCount * removedMid
removedSum += removedCount * bucketMidpoint

Comment thread promql/engine.go
Comment on lines +3082 to +3083
removedMid := bucketMidpoint
removedSum += removedCount * removedMid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
removedMid := bucketMidpoint
removedSum += removedCount * removedMid
removedSum += removedCount * bucketMidpoint

Comment thread promql/engine.go
product := math.Abs(b.Lower) * math.Abs(rhs)
underCount := computeSplit(b, rhs, isPositive, isCustomBucket)
if op == parser.TRIM_UPPER {
return underCount, computeMidpoint(b, product, isCustomBucket, isPositive)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems very suspicious that for custom buckets we use the Lower and Upper bound for the mid point, but for exponential buckets we go from the lower bound to the rhs (or rhs to upper bound) for the mid point calculation.

Feels like one of those is wrong.

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.

@krajorama yes, I also struggle to justify this (esp. the fact that for exponential, we are only using a single bucket bound to interpolate). After some digging, it seems that the original author based this implementation on this comment by @beorn7 : #16330 (comment)
So I think it would be useful to get additional feedback from @beorn7 on this interpolation formula.

Comment thread promql/engine.go
trimmedHist.Compact(0)
}

func computeMidpoint(b histogram.Bucket[float64], product float64, isCustom, isPositive bool) float64 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Following from my comment above I would expect this function to simply look like this:

func computeMidpoint(a, b float64, isLinear, isPositive bool) float64 {
	if isLinear {
		return (b + a) / 2
	}

	if isPositive {
		return math.Sqrt(math.Abs(a) * math.Abs(b))
	}
	return -math.Sqrt(math.Abs(a) * math.Abs(b))
}

But I could not get every test to pass. So I think that question needs answering about why we use different kind of ranges when calling this function. Bucket lower/upper bound for custom buckets, but range adjusted with rhs for the exponential buckets.

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.

Let's contain this discussion in the related comment above: #16330 (comment)

@beorn7

beorn7 commented Aug 19, 2025

Copy link
Copy Markdown
Contributor

@sujalshah-bit just to let you know: @krajorama is on vacations for the rest of the month, but I'll continue the review once you have addressed @krajorama's latest comments. Just leave a note here once the PR is ready for the next round of review.

@beorn7

beorn7 commented Sep 17, 2025

Copy link
Copy Markdown
Contributor

@sujalshah-bit are you still planning to address @krajorama's comments?

@sujalshah-bit

Copy link
Copy Markdown
Contributor Author

Yes, I am planning to do it, but not any time soon. If someone wants to pick the issue, please feel free to take the PR forward.

@linasm

linasm commented Jan 15, 2026

Copy link
Copy Markdown
Contributor

Yes, I am planning to do it, but not any time soon. If someone wants to pick the issue, please feel free to take the PR forward.

@sujalshah-bit (also @beorn7 and @krajorama ) I see no progress on this PR for almost 6 months now. If there are no objections, I can pick it up and try taking it across the finish line.
I would start by addressing the existing comments.
Also, after looking a the code, I am tempted to make trimHistogram a method of FloatHistogram, where it would fit nicely with other histogram operations (Mul, Div, etc.). Moving it (and its helper functions) to https://github.com/prometheus/prometheus/blob/main/model/histogram/float_histogram.go would avoid polluting engine.go (which already is so huge) with few hundred LoC of histogram specific code. This move would be done separately from addressing the existing comments. Also, would decouple the implementation from parser.ItemType argument.

@linasm

linasm commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

I think we're getting close. I have a bunch of nitpicks, a comment on the hasPositive/hasNegative calc and potentially an issue with the sum / midpoint calc.

@krajorama I have addressed this whole set of your comments (except for the interpolation question in #16330 (comment) which I think will require some further discussion).

I don't have push rights in the original author's repository, so I have pushed my changes (all in a separate commit) to my own repo. Would appreciate if one of the maintainers could update this PR to point to the branch on my repo (linasm:trim_histogram), I hope this will keep the comment history in place.

@linasm

linasm commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

@beorn7 @krajorama I had no way of pushing the changes into this original branch/PR so I have opened a new PR including all the original work plus my current addition (addressing @krajorama 's comments). Can we move further discussion to #17904 (and perhaps close this one or convert it to draft to avoid confusion)?

@beorn7

beorn7 commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

Closing this in lieu of #17904 .

Thank you very much @linasm .

@beorn7 beorn7 closed this Jan 21, 2026
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.

native histograms: Implement the </ and >/ operators to "trim" a histogram.

7 participants