PromQL: Add experimental histogram_quantiles variadic function (#17285)
Some checks failed
buf.build / lint and publish (push) Has been cancelled
CI / Go tests (push) Has been cancelled
CI / More Go tests (push) Has been cancelled
CI / Go tests with previous Go version (push) Has been cancelled
CI / UI tests (push) Has been cancelled
CI / Go tests on Windows (push) Has been cancelled
CI / Mixins tests (push) Has been cancelled
CI / Build Prometheus for common architectures (push) Has been cancelled
CI / Build Prometheus for all architectures (push) Has been cancelled
CI / Check generated parser (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
CI / fuzzing (push) Has been cancelled
CI / codeql (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CI / Report status of build Prometheus for all architectures (push) Has been cancelled
CI / Publish main branch artifacts (push) Has been cancelled
CI / Publish release artefacts (push) Has been cancelled
CI / Publish UI on npm Registry (push) Has been cancelled

Signed-off-by: Linas Medziunas <linas.medziunas@gmail.com>
Signed-off-by: Björn Rabenstein <github@rabenste.in>
Signed-off-by: beorn7 <beorn@grafana.com>
Co-authored-by: Björn Rabenstein <github@rabenste.in>
Co-authored-by: beorn7 <beorn@grafana.com>
This commit is contained in:
Linas Medžiūnas 2026-02-18 18:32:29 +02:00 committed by GitHub
parent ece9437624
commit 5bd0d00f8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 292 additions and 4 deletions

View file

@ -80,6 +80,7 @@
"histogram_count": true,
"histogram_fraction": true,
"histogram_quantile": true,
"histogram_quantiles": false,
"histogram_stddev": true,
"histogram_stdvar": true,
"histogram_sum": true,

View file

@ -433,6 +433,23 @@ and is therefore flagged by an info-level annotation reading `input to
histogram_quantile needed to be fixed for monotonicity`. If you encounter this
annotation, you should find and remove the source of the invalid data.
## `histogram_quantiles()`
**This function has to be enabled via the [feature
flag](../feature_flags.md#experimental-promql-functions)
`--enable-feature=promql-experimental-functions`.**
`histogram_quantiles(v instant-vector, quantile_label string, φ_1 scalar, φ_2 scalar, ...)` calculates multiple (between 1 and 10) φ-quantiles (0 ≤
φ ≤ 1) from a [classic
histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) or from
a native histogram. Quantile calculation works the same way as in `histogram_quantile()`.
The second argument (a string) specifies the label name that is used to identify different quantiles in the query result.
```
histogram_quantiles(sum(rate(foo[1m])), "quantile", 0.9, 0.99)
# => {quantile="0.9"} 123
{quantile="0.99"} 128
```
## `histogram_stddev()` and `histogram_stdvar()`
`histogram_stddev(v instant-vector)` returns the estimated standard deviation

View file

@ -1214,6 +1214,9 @@ type EvalNodeHelper struct {
// funcHistogramQuantile and funcHistogramFraction for classic histograms.
signatureToMetricWithBuckets map[string]*metricWithBuckets
nativeHistogramSamples []Sample
// funcHistogramQuantiles for histograms.
quantileStrs map[float64]string
signatureToLabelsWithQuantile map[string]map[float64]labels.Labels
lb *labels.Builder
lblBuf []byte
@ -1305,6 +1308,35 @@ func (enh *EvalNodeHelper) resetHistograms(inVec Vector, arg parser.Expr) annota
return annos
}
func (enh *EvalNodeHelper) getOrCreateLblsWithQuantile(lbls labels.Labels, quantileLabel string, q float64) labels.Labels {
if enh.signatureToLabelsWithQuantile == nil {
enh.signatureToLabelsWithQuantile = make(map[string]map[float64]labels.Labels)
}
enh.lblBuf = lbls.Bytes(enh.lblBuf)
cachedLbls, ok := enh.signatureToLabelsWithQuantile[string(enh.lblBuf)]
if !ok {
cachedLbls = make(map[float64]labels.Labels, len(enh.quantileStrs))
enh.signatureToLabelsWithQuantile[string(enh.lblBuf)] = cachedLbls
}
cachedLblsWithQuantile, ok := cachedLbls[q]
if !ok {
quantileStr := "NaN"
if !math.IsNaN(q) {
// Cannot do map lookup by NaN key.
quantileStr = enh.quantileStrs[q]
}
cachedLblsWithQuantile = labels.NewBuilder(lbls).
Set(quantileLabel, quantileStr).
Labels()
cachedLbls[q] = cachedLblsWithQuantile
}
return cachedLblsWithQuantile
}
// rangeEval evaluates the given expressions, and then for each step calls
// the given funcCall with the values computed for each expression at that
// step. The return value is the combination into time series of all the
@ -4320,7 +4352,7 @@ func detectHistogramStatsDecoding(expr parser.Expr) {
// further up (the latter wouldn't make sense,
// but no harm in detecting it).
n.SkipHistogramBuckets = true
case "histogram_quantile", "histogram_fraction":
case "histogram_quantile", "histogram_quantiles", "histogram_fraction":
// If we ever see a function that needs the
// whole histogram, we will not skip the
// buckets.

View file

@ -1720,8 +1720,8 @@ func funcHistogramQuantile(vectorVals []Vector, _ Matrix, args parser.Expression
inVec := vectorVals[1]
var annos annotations.Annotations
if math.IsNaN(q) || q < 0 || q > 1 {
annos.Add(annotations.NewInvalidQuantileWarning(q, args[0].PositionRange()))
if err := validateQuantile(q, args[0]); err != nil {
annos.Add(err)
}
annos.Merge(enh.resetHistograms(inVec, args[1]))
@ -1770,6 +1770,89 @@ func funcHistogramQuantile(vectorVals []Vector, _ Matrix, args parser.Expression
return enh.Out, annos
}
func validateQuantile(q float64, arg parser.Expr) error {
if math.IsNaN(q) || q < 0 || q > 1 {
return annotations.NewInvalidQuantileWarning(q, arg.PositionRange())
}
return nil
}
// === histogram_quantiles(Vector parser.ValueTypeVector, label parser.ValueTypeString, q0 parser.ValueTypeScalar, qs parser.ValueTypeScalar...) (Vector, Annotations) ===
func funcHistogramQuantiles(vectorVals []Vector, _ Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
var (
inVec = vectorVals[0]
quantileLabel = args[1].(*parser.StringLiteral).Val
numQuantiles = len(vectorVals[2:])
qs = make([]float64, 0, numQuantiles)
annos annotations.Annotations
)
if enh.quantileStrs == nil {
enh.quantileStrs = make(map[float64]string, numQuantiles)
}
for i := 2; i < len(vectorVals); i++ {
q := vectorVals[i][0].F
if err := validateQuantile(q, args[i]); err != nil {
annos.Add(err)
}
if _, ok := enh.quantileStrs[q]; !ok {
enh.quantileStrs[q] = labels.FormatOpenMetricsFloat(q)
}
qs = append(qs, q)
}
annos.Merge(enh.resetHistograms(inVec, args[0]))
for _, q := range qs {
// Deal with the native histograms.
for _, sample := range enh.nativeHistogramSamples {
if sample.H == nil {
// Native histogram conflicts with classic histogram at the same timestamp, ignore.
continue
}
if !enh.enableDelayedNameRemoval {
sample.Metric = sample.Metric.DropReserved(schema.IsMetadataLabel)
}
hq, hqAnnos := HistogramQuantile(q, sample.H, sample.Metric.Get(model.MetricNameLabel), args[0].PositionRange())
annos.Merge(hqAnnos)
enh.Out = append(enh.Out, Sample{
Metric: enh.getOrCreateLblsWithQuantile(sample.Metric, quantileLabel, q),
F: hq,
DropName: true,
})
}
// Deal with classic histograms that have already been filtered for conflicting native histograms.
for _, mb := range enh.signatureToMetricWithBuckets {
if len(mb.buckets) > 0 {
hq, forcedMonotonicity, _, minBucket, maxBucket, maxDiff := BucketQuantile(q, mb.buckets)
if forcedMonotonicity {
metricName := ""
if enh.enableDelayedNameRemoval {
metricName = getMetricName(mb.metric)
}
annos.Add(annotations.NewHistogramQuantileForcedMonotonicityInfo(metricName, args[1].PositionRange(), enh.Ts, minBucket, maxBucket, maxDiff))
}
if !enh.enableDelayedNameRemoval {
mb.metric = mb.metric.DropReserved(schema.IsMetadataLabel)
}
enh.Out = append(enh.Out, Sample{
Metric: enh.getOrCreateLblsWithQuantile(mb.metric, quantileLabel, q),
F: hq,
DropName: true,
})
}
}
}
return enh.Out, annos
}
// pickFirstSampleIndex returns the index of the last sample before
// or at the range start, or 0 if none exist before the range start.
// If the vector selector is not anchored, it always returns 0, true.
@ -2100,6 +2183,7 @@ var FunctionCalls = map[string]FunctionCall{
"histogram_count": funcHistogramCount,
"histogram_fraction": funcHistogramFraction,
"histogram_quantile": funcHistogramQuantile,
"histogram_quantiles": funcHistogramQuantiles,
"histogram_sum": funcHistogramSum,
"histogram_stddev": funcHistogramStdDev,
"histogram_stdvar": funcHistogramStdVar,

View file

@ -205,6 +205,13 @@ var Functions = map[string]*Function{
ArgTypes: []ValueType{ValueTypeScalar, ValueTypeVector},
ReturnType: ValueTypeVector,
},
"histogram_quantiles": {
Name: "histogram_quantiles",
ArgTypes: []ValueType{ValueTypeVector, ValueTypeString, ValueTypeScalar, ValueTypeScalar},
Variadic: 9,
ReturnType: ValueTypeVector,
Experimental: true,
},
"double_exponential_smoothing": {
Name: "double_exponential_smoothing",
ArgTypes: []ValueType{ValueTypeMatrix, ValueTypeScalar, ValueTypeScalar},

View file

@ -598,6 +598,40 @@ eval instant at 50m histogram_quantile(1, testhistogram3_bucket)
{start="positive"} 1
{start="negative"} -0.1
eval instant at 50m histogram_quantiles(testhistogram3, "q", 0, 0.25, 0.5, 0.75, 1)
expect no_warn
{q="0.0", start="positive"} 0
{q="0.0", start="negative"} -0.25
{q="0.25", start="positive"} 0.055
{q="0.25", start="negative"} -0.225
{q="0.5", start="positive"} 0.125
{q="0.5", start="negative"} -0.2
{q="0.75", start="positive"} 0.45
{q="0.75", start="negative"} -0.15
{q="1.0", start="positive"} 1
{q="1.0", start="negative"} -0.1
eval instant at 50m histogram_quantiles(testhistogram3_bucket, "q", 0, 0.25, 0.5, 0.75, 1)
expect no_warn
{q="0.0", start="positive"} 0
{q="0.0", start="negative"} -0.25
{q="0.25", start="positive"} 0.055
{q="0.25", start="negative"} -0.225
{q="0.5", start="positive"} 0.125
{q="0.5", start="negative"} -0.2
{q="0.75", start="positive"} 0.45
{q="0.75", start="negative"} -0.15
{q="1.0", start="positive"} 1
{q="1.0", start="negative"} -0.1
# Break label set uniqueness.
eval instant at 50m histogram_quantiles(testhistogram3, "start", 0, 0.25, 0.5, 0.75, 1)
expect fail
eval instant at 50m histogram_quantiles(testhistogram3_bucket, "start", 0, 0.25, 0.5, 0.75, 1)
expect fail
# Quantile too low.
eval instant at 50m histogram_quantile(-0.1, testhistogram)
@ -610,6 +644,16 @@ eval instant at 50m histogram_quantile(-0.1, testhistogram_bucket)
{start="positive"} -Inf
{start="negative"} -Inf
eval instant at 50m histogram_quantiles(testhistogram, "q", -0.1)
expect warn
{q="-0.1", start="positive"} -Inf
{q="-0.1", start="negative"} -Inf
eval instant at 50m histogram_quantiles(testhistogram_bucket, "q", -0.1)
expect warn
{q="-0.1", start="positive"} -Inf
{q="-0.1", start="negative"} -Inf
# Quantile too high.
eval instant at 50m histogram_quantile(1.01, testhistogram)
@ -622,6 +666,16 @@ eval instant at 50m histogram_quantile(1.01, testhistogram_bucket)
{start="positive"} +Inf
{start="negative"} +Inf
eval instant at 50m histogram_quantiles(testhistogram, "q", 1.01)
expect warn
{q="1.01", start="positive"} +Inf
{q="1.01", start="negative"} +Inf
eval instant at 50m histogram_quantiles(testhistogram_bucket, "q", 1.01)
expect warn
{q="1.01", start="positive"} +Inf
{q="1.01", start="negative"} +Inf
# Quantile invalid.
eval instant at 50m histogram_quantile(NaN, testhistogram)
@ -634,9 +688,22 @@ eval instant at 50m histogram_quantile(NaN, testhistogram_bucket)
{start="positive"} NaN
{start="negative"} NaN
eval instant at 50m histogram_quantiles(testhistogram, "q", NaN)
expect warn
{q="NaN", start="positive"} NaN
{q="NaN", start="negative"} NaN
eval instant at 50m histogram_quantiles(testhistogram_bucket, "q", NaN)
expect warn
{q="NaN", start="positive"} NaN
{q="NaN", start="negative"} NaN
eval instant at 50m histogram_quantile(NaN, non_existent)
expect warn msg: PromQL warning: quantile value should be between 0 and 1, got NaN
eval instant at 50m histogram_quantiles(non_existent, "q", NaN)
expect warn msg: PromQL warning: quantile value should be between 0 and 1, got NaN
# Quantile value in lowest bucket.
eval instant at 50m histogram_quantile(0, testhistogram)
@ -967,6 +1034,12 @@ eval instant at 50m histogram_quantile(0.99, nonmonotonic_bucket)
expect info
{} 979.75
eval instant at 50m histogram_quantiles(nonmonotonic_bucket, "q", 0.01, 0.5, 0.99)
expect info
{q="0.01"} 0.0045
{q="0.5"} 8.5
{q="0.99"} 979.75
# Buckets with different representations of the same upper bound.
eval instant at 50m histogram_quantile(0.5, rate(mixed_bucket[10m]))
{instance="ins1", job="job1"} 0.15
@ -1002,9 +1075,15 @@ load_with_nhcb 5m
eval instant at 50m histogram_quantile(0.99, {__name__=~"request_duration_seconds\\d*_bucket"})
expect fail
eval instant at 50m histogram_quantiles({__name__=~"request_duration_seconds\\d*_bucket"}, "q", 0.99)
expect fail
eval instant at 50m histogram_quantile(0.99, {__name__=~"request_duration_seconds\\d*"})
expect fail
eval instant at 50m histogram_quantiles({__name__=~"request_duration_seconds\\d*"}, "q", 0.99)
expect fail
# Histogram with constant buckets.
load_with_nhcb 1m
const_histogram_bucket{le="0.0"} 1 1 1 1 1
@ -1066,7 +1145,7 @@ eval instant at 10m histogram_sum(increase(histogram_with_reset[15m]))
clear
# Test histogram_quantile and histogram_fraction with conflicting classic and native histograms.
# Test histogram_quantile(s) and histogram_fraction with conflicting classic and native histograms.
load 1m
series{host="a"} {{schema:0 sum:5 count:4 buckets:[9 2 1]}}
series{host="a", le="0.1"} 2
@ -1081,6 +1160,11 @@ eval instant at 0 histogram_quantile(0.8, series)
expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "series"
# Should return no results.
eval instant at 0 histogram_quantiles(series, "q", 0.1, 0.2)
expect no_info
expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "series"
# Should return no results.
eval instant at 0 histogram_fraction(-Inf, 1, series)
expect no_info
expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "series"

View file

@ -55,6 +55,10 @@ eval instant at 1m histogram_quantile(0.5, single_histogram)
expect no_info
{} 1.414213562373095
eval instant at 1m histogram_quantiles(single_histogram, "q", 0.5)
expect no_info
{q="0.5"} 1.414213562373095
clear
# Repeat the same histogram 10 times.
@ -1605,6 +1609,11 @@ eval instant at 1m histogram_quantile(0.81, histogram_nan)
{case="100% NaNs"} NaN
{case="20% NaNs"} NaN
eval instant at 1m histogram_quantiles(histogram_nan, "q", 0.81)
expect info msg: PromQL info: input to histogram_quantile has NaN observations, result is NaN for metric name "histogram_nan"
{case="100% NaNs", q="0.81"} NaN
{case="20% NaNs", q="0.81"} NaN
eval instant at 1m histogram_quantile(0.8, histogram_nan{case="100% NaNs"})
expect info msg: PromQL info: input to histogram_quantile has NaN observations, result is NaN for metric name "histogram_nan"
{case="100% NaNs"} NaN
@ -1891,6 +1900,9 @@ eval instant at 1m histogram_quantile(0.5, myHistogram2)
eval instant at 1m histogram_quantile(0.5, mixedHistogram)
expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "mixedHistogram"
eval instant at 1m histogram_quantiles(mixedHistogram, "q", 0.5)
expect warn msg: PromQL warning: vector contains a mix of classic and native histograms for metric name "mixedHistogram"
clear
# A counter reset only in a bucket. Sub-queries still need to detect
@ -1960,6 +1972,9 @@ eval instant at 1m histogram_count(histogram unless histogram_quantile(0.5, hist
eval instant at 1m histogram_quantile(0.5, histogram unless histogram_count(histogram) == 0)
{} 3.1748021039363987
eval instant at 1m histogram_quantiles(histogram unless histogram_count(histogram) == 0, "q", 0.5)
{q="0.5"} 3.1748021039363987
clear
# Regression test for:

View file

@ -1543,6 +1543,33 @@ const funcDocs: Record<string, React.ReactNode> = {
</p>
</>
),
histogram_quantiles: (
<>
<p>
<strong>
This function has to be enabled via the{" "}
<a href="../feature_flags.md#experimental-promql-functions">feature flag</a>
<code>--enable-feature=promql-experimental-functions</code>.
</strong>
</p>
<p>
<code>histogram_quantiles(v instant-vector, quantile_label string, φ_1 scalar, φ_2 scalar, ...)</code>{" "}
calculates multiple (between 1 and 10) φ-quantiles (0 φ 1) from a{" "}
<a href="https://prometheus.io/docs/concepts/metric_types/#histogram">classic histogram</a> or from a native
histogram. Quantile calculation works the same way as in <code>histogram_quantile()</code>. The second argument
(a string) specifies the label name that is used to identify different quantiles in the query result.
</p>
<pre>
<code>
histogram_quantiles(sum(rate(foo[1m])), &quot;quantile&quot;, 0.9, 0.99) # =&gt; {"{"}quantile=&quot;0.9&quot;
{"}"} 123
{"{"}quantile=&quot;0.99&quot;{"}"} 128
</code>
</pre>
</>
),
histogram_stddev: (
<>
<p>

View file

@ -69,6 +69,12 @@ export const functionSignatures: Record<string, Func> = {
variadic: 0,
returnType: valueType.vector,
},
histogram_quantiles: {
name: "histogram_quantiles",
argTypes: [valueType.vector, valueType.string, valueType.scalar, valueType.scalar],
variadic: 9,
returnType: valueType.vector,
},
histogram_stddev: {
name: "histogram_stddev",
argTypes: [valueType.vector],

View file

@ -243,6 +243,12 @@ export const functionIdentifierTerms = [
info: 'Calculate quantiles from native histograms and from conventional histogram buckets',
type: 'function',
},
{
label: 'histogram_quantiles',
detail: 'function',
info: 'Calculate multiple quantiles from native histograms and from conventional histogram buckets',
type: 'function',
},
{
label: 'histogram_sum',
detail: 'function',

View file

@ -44,6 +44,7 @@ import {
HistogramCount,
HistogramFraction,
HistogramQuantile,
HistogramQuantiles,
HistogramStdDev,
HistogramStdVar,
HistogramSum,
@ -306,6 +307,12 @@ const promqlFunctions: { [key: number]: PromQLFunction } = {
variadic: 0,
returnType: ValueType.vector,
},
[HistogramQuantiles]: {
name: 'histogram_quantiles',
argTypes: [ValueType.vector, ValueType.string, ValueType.scalar, ValueType.scalar],
variadic: 10,
returnType: ValueType.vector,
},
[HistogramStdDev]: {
name: 'histogram_stddev',
argTypes: [ValueType.vector],

View file

@ -167,6 +167,7 @@ FunctionIdentifier {
HistogramCount |
HistogramFraction |
HistogramQuantile |
HistogramQuantiles |
HistogramStdDev |
HistogramStdVar |
HistogramSum |
@ -426,6 +427,7 @@ NumberDurationLiteralInDurationContext {
HistogramCount { condFn<"histogram_count"> }
HistogramFraction { condFn<"histogram_fraction"> }
HistogramQuantile { condFn<"histogram_quantile"> }
HistogramQuantiles { condFn<"histogram_quantiles"> }
HistogramStdDev { condFn<"histogram_stddev"> }
HistogramStdVar { condFn<"histogram_stdvar"> }
HistogramSum { condFn<"histogram_sum"> }