app/vmselect/promql: return lower and upper bounds for the estimated percentile from histogram_quantile if third arg is passed

Updates https://github.com/prometheus/prometheus/issues/5706
This commit is contained in:
Aliaksandr Valialkin 2019-12-11 13:55:18 +02:00
parent 07d5bc986b
commit 73b2a3d4b7
3 changed files with 126 additions and 13 deletions

View File

@ -2324,6 +2324,35 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r} resultExpected := []netstorage.Result{r}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run(`histogram_quantile(single-value-valid-le, boundsLabel)`, func(t *testing.T) {
t.Parallel()
q := `sort(histogram_quantile(0.6, label_set(100, "le", "200"), "foobar"))`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{0, 0, 0, 0, 0, 0},
Timestamps: timestampsExpected,
}
r1.MetricName.Tags = []storage.Tag{{
Key: []byte("foobar"),
Value: []byte("lower"),
}}
r2 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{120, 120, 120, 120, 120, 120},
Timestamps: timestampsExpected,
}
r3 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{200, 200, 200, 200, 200, 200},
Timestamps: timestampsExpected,
}
r3.MetricName.Tags = []storage.Tag{{
Key: []byte("foobar"),
Value: []byte("upper"),
}}
resultExpected := []netstorage.Result{r1, r2, r3}
f(q, resultExpected)
})
t.Run(`histogram_quantile(single-value-valid-le-max-phi)`, func(t *testing.T) { t.Run(`histogram_quantile(single-value-valid-le-max-phi)`, func(t *testing.T) {
t.Parallel() t.Parallel()
q := `histogram_quantile(1, ( q := `histogram_quantile(1, (
@ -2461,6 +2490,56 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r} resultExpected := []netstorage.Result{r}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run(`histogram_quantile(normal-bucket-count, boundsLabel)`, func(t *testing.T) {
t.Parallel()
q := `sort(histogram_quantile(0.2,
label_set(0, "foo", "bar", "le", "10")
or label_set(100, "foo", "bar", "le", "30")
or label_set(300, "foo", "bar", "le", "+Inf"),
"xxx"
))`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{10, 10, 10, 10, 10, 10},
Timestamps: timestampsExpected,
}
r1.MetricName.Tags = []storage.Tag{
{
Key: []byte("foo"),
Value: []byte("bar"),
},
{
Key: []byte("xxx"),
Value: []byte("lower"),
},
}
r2 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{22, 22, 22, 22, 22, 22},
Timestamps: timestampsExpected,
}
r2.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("bar"),
}}
r3 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{30, 30, 30, 30, 30, 30},
Timestamps: timestampsExpected,
}
r3.MetricName.Tags = []storage.Tag{
{
Key: []byte("foo"),
Value: []byte("bar"),
},
{
Key: []byte("xxx"),
Value: []byte("upper"),
},
}
resultExpected := []netstorage.Result{r1, r2, r3}
f(q, resultExpected)
})
t.Run(`histogram_quantile(zero-bucket-count)`, func(t *testing.T) { t.Run(`histogram_quantile(zero-bucket-count)`, func(t *testing.T) {
t.Parallel() t.Parallel()
q := `histogram_quantile(0.6, q := `histogram_quantile(0.6,

View File

@ -400,17 +400,27 @@ func vmrangeBucketsToLE(tss []*timeseries) []*timeseries {
func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) { func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args args := tfa.args
if err := expectTransformArgsNum(args, 2); err != nil { if len(args) < 2 || len(args) > 3 {
return nil, err return nil, fmt.Errorf("unexpected number of args; got %d; want 2...3", len(args))
} }
phis, err := getScalar(args[0], 0) phis, err := getScalar(args[0], 0)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("cannot parse phi: %s", err)
} }
// Convert buckets with `vmrange` labels to buckets with `le` labels. // Convert buckets with `vmrange` labels to buckets with `le` labels.
tss := vmrangeBucketsToLE(args[1]) tss := vmrangeBucketsToLE(args[1])
// Parse boundsLabel. See https://github.com/prometheus/prometheus/issues/5706 for details.
var boundsLabel string
if len(args) > 2 {
s, err := getString(args[2], 2)
if err != nil {
return nil, fmt.Errorf("cannot parse boundsLabel (arg #3): %s", err)
}
boundsLabel = s
}
// Group metrics by all tags excluding "le" // Group metrics by all tags excluding "le"
type x struct { type x struct {
le float64 le float64
@ -453,10 +463,10 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
} }
return nan return nan
} }
quantile := func(i int, phis []float64, xss []x) float64 { quantile := func(i int, phis []float64, xss []x) (q, lower, upper float64) {
phi := phis[i] phi := phis[i]
if math.IsNaN(phi) { if math.IsNaN(phi) {
return nan return nan, nan, nan
} }
// Fix broken buckets. // Fix broken buckets.
// They are already sorted by le, so their values must be in ascending order, // They are already sorted by le, so their values must be in ascending order,
@ -479,13 +489,13 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
xss = xss[:len(xss)-1] xss = xss[:len(xss)-1]
} }
if vLast == 0 || math.IsNaN(vLast) { if vLast == 0 || math.IsNaN(vLast) {
return nan return nan, nan, nan
} }
if phi < 0 { if phi < 0 {
return -inf return -inf, -inf, xss[0].ts.Values[i]
} }
if phi > 1 { if phi > 1 {
return inf return inf, vLast, inf
} }
vReq := vLast * phi vReq := vLast * phi
vPrev = 0 vPrev = 0
@ -509,14 +519,17 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
continue continue
} }
if math.IsInf(le, 0) { if math.IsInf(le, 0) {
return lastNonInf(i, xss) vv := lastNonInf(i, xss)
return vv, vv, inf
} }
if v == vPrev { if v == vPrev {
return lePrev return lePrev, lePrev, v
} }
return lePrev + (le-lePrev)*(vReq-vPrev)/(v-vPrev) vv := lePrev + (le-lePrev)*(vReq-vPrev)/(v-vPrev)
return vv, lePrev, le
} }
return lastNonInf(i, xss) vv := lastNonInf(i, xss)
return vv, vv, inf
} }
rvs := make([]*timeseries, 0, len(m)) rvs := make([]*timeseries, 0, len(m))
for _, xss := range m { for _, xss := range m {
@ -524,10 +537,30 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
return xss[i].le < xss[j].le return xss[i].le < xss[j].le
}) })
dst := xss[0].ts dst := xss[0].ts
var tsLower, tsUpper *timeseries
if len(boundsLabel) > 0 {
tsLower = &timeseries{}
tsLower.CopyFromShallowTimestamps(dst)
tsLower.MetricName.RemoveTag(boundsLabel)
tsLower.MetricName.AddTag(boundsLabel, "lower")
tsUpper = &timeseries{}
tsUpper.CopyFromShallowTimestamps(dst)
tsUpper.MetricName.RemoveTag(boundsLabel)
tsUpper.MetricName.AddTag(boundsLabel, "upper")
}
for i := range dst.Values { for i := range dst.Values {
dst.Values[i] = quantile(i, phis, xss) v, lower, upper := quantile(i, phis, xss)
dst.Values[i] = v
if len(boundsLabel) > 0 {
tsLower.Values[i] = lower
tsUpper.Values[i] = upper
}
} }
rvs = append(rvs, dst) rvs = append(rvs, dst)
if len(boundsLabel) > 0 {
rvs = append(rvs, tsLower)
rvs = append(rvs, tsUpper)
}
} }
return rvs, nil return rvs, nil
} }

View File

@ -12,6 +12,7 @@ Try these extensions on [an editable Grafana dashboard](http://play-grafana.vict
- `offset` may be put anywere in the query. For instance, `sum(foo) offset 24h`. - `offset` may be put anywere in the query. For instance, `sum(foo) offset 24h`.
- `offset` may be negative. For example, `q offset -1h`. - `offset` may be negative. For example, `q offset -1h`.
- `default` binary operator. `q1 default q2` substitutes `NaN` values from `q1` with the corresponding values from `q2`. - `default` binary operator. `q1 default q2` substitutes `NaN` values from `q1` with the corresponding values from `q2`.
- `histogram_quantile` accepts optional third arg - `boundsLabel`. In this case it returns `lower` and `upper` bounds for the estimated percentile. See [this issue for details](https://github.com/prometheus/prometheus/issues/5706).
- `if` binary operator. `q1 if q2` removes values from `q1` for `NaN` values from `q2`. - `if` binary operator. `q1 if q2` removes values from `q1` for `NaN` values from `q2`.
- `ifnot` binary operator. `q1 ifnot q2` removes values from `q1` for non-`NaN` values from `q2`. - `ifnot` binary operator. `q1 ifnot q2` removes values from `q1` for non-`NaN` values from `q2`.
- Trailing commas on all the lists are allowed - label filters, function args and with expressions. For instance, the following queries are valid: `m{foo="bar",}`, `f(a, b,)`, `WITH (x=y,) x`. This simplifies maintenance of multi-line queries. - Trailing commas on all the lists are allowed - label filters, function args and with expressions. For instance, the following queries are valid: `m{foo="bar",}`, `f(a, b,)`, `WITH (x=y,) x`. This simplifies maintenance of multi-line queries.