diff --git a/app/vmselect/promql/eval.go b/app/vmselect/promql/eval.go index ea059f3cb..c38a95bc8 100644 --- a/app/vmselect/promql/eval.go +++ b/app/vmselect/promql/eval.go @@ -650,6 +650,9 @@ func evalRollupFuncWithoutAt(ec *EvalConfig, funcName string, rf rollupFunc, exp if err != nil { return nil, err } + if funcName == "absent_over_time" { + rvs = aggregateAbsentOverTime(ec, re.Expr, rvs) + } ec.updateIsPartialResponse(ecNew.IsPartialResponse) if offset != 0 && len(rvs) > 0 { // Make a copy of timestamps, since they may be used in other values. @@ -665,6 +668,27 @@ func evalRollupFuncWithoutAt(ec *EvalConfig, funcName string, rf rollupFunc, exp return rvs, nil } +// aggregateAbsentOverTime collapses tss to a single time series with 1 and nan values. +// +// Values for returned series are set to nan if at least a single tss series contains nan at that point. +// This means that tss contains a series with non-empty results at that point. +// This follows Prometheus logic - see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2130 +func aggregateAbsentOverTime(ec *EvalConfig, expr metricsql.Expr, tss []*timeseries) []*timeseries { + rvs := getAbsentTimeseries(ec, expr) + if len(tss) == 0 { + return rvs + } + for i := range tss[0].Values { + for _, ts := range tss { + if math.IsNaN(ts.Values[i]) { + rvs[0].Values[i] = nan + break + } + } + } + return rvs +} + func evalRollupFuncWithSubquery(ec *EvalConfig, funcName string, rf rollupFunc, expr metricsql.Expr, re *metricsql.RollupExpr) ([]*timeseries, error) { // TODO: determine whether to use rollupResultCacheV here. step := re.Step.Duration(ec.Step) @@ -688,10 +712,6 @@ func evalRollupFuncWithSubquery(ec *EvalConfig, funcName string, rf rollupFunc, } ec.updateIsPartialResponse(ecSQ.IsPartialResponse) if len(tssSQ) == 0 { - if funcName == "absent_over_time" { - tss := evalNumber(ec, 1) - return tss, nil - } return nil, nil } sharedTimestamps := getTimestamps(ec.Start, ec.End, ec.Step) @@ -842,14 +862,7 @@ func evalRollupFuncWithMetricExpr(ec *EvalConfig, funcName string, rf rollupFunc rssLen := rss.Len() if rssLen == 0 { rss.Cancel() - var tss []*timeseries - if funcName == "absent_over_time" { - tss = getAbsentTimeseries(ec, me) - } - // Add missing points until ec.End. - // Do not cache the result, since missing points - // may be backfilled in the future. - tss = mergeTimeseries(tssCached, tss, start, ec) + tss := mergeTimeseries(tssCached, nil, start, ec) return tss, nil } diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go index 4c02a3710..c7f09f4d6 100644 --- a/app/vmselect/promql/exec_test.go +++ b/app/vmselect/promql/exec_test.go @@ -877,19 +877,37 @@ func TestExecSuccess(t *testing.T) { resultExpected := []netstorage.Result{r} f(q, resultExpected) }) - t.Run(`absent_over_time(scalar(multi-timeseries))`, func(t *testing.T) { + t.Run(`absent_over_time(non-nan)`, func(t *testing.T) { t.Parallel() q := ` - absent_over_time(label_set(scalar(1 or label_set(2, "xx", "foo")), "yy", "foo"))` + absent_over_time(time())` + resultExpected := []netstorage.Result{} + f(q, resultExpected) + }) + t.Run(`absent_over_time(nan)`, func(t *testing.T) { + t.Parallel() + q := ` + absent_over_time((time() < 1500)[300s:])` r := netstorage.Result{ MetricName: metricNameExpected, - Values: []float64{1, 1, 1, 1, 1, 1}, + Values: []float64{nan, nan, nan, nan, 1, 1}, + Timestamps: timestampsExpected, + } + resultExpected := []netstorage.Result{r} + f(q, resultExpected) + }) + t.Run(`absent_over_time(multi-ts)`, func(t *testing.T) { + t.Parallel() + q := ` + absent_over_time(( + alias((time() < 1400)[200s:], "one"), + alias((time() > 1600)[200s:], "two"), + ))` + r := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{nan, nan, nan, 1, nan, nan}, Timestamps: timestampsExpected, } - r.MetricName.Tags = []storage.Tag{{ - Key: []byte("yy"), - Value: []byte("foo"), - }} resultExpected := []netstorage.Result{r} f(q, resultExpected) }) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b045e5d7b..2527f6106 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -34,6 +34,7 @@ The following tip changes can be tested by building VictoriaMetrics components f * FEATURE: add `__meta_kubernetes_endpointslice_label*` and `__meta_kubernetes_endpointslice_annotation*` labels for `role: endpointslice` targets in [kubernetes_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config) to be consistent with other `role` values. See [this issue](https://github.com/prometheus/prometheus/issues/10284). * FEATURE: vmagent: support Prometheus-like durations in `-promscrape.config`. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/817#issuecomment-1033384766). +* BUGFIX: calculate [absent_over_time()](https://docs.victoriametrics.com/MetricsQL.html#absent_over_time) in the same way as Prometheus does. Previously it could return multiple time series instead of at most one time series like Prometheus does. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2130). * BUGFIX: return proper results from `highestMax()` function at [Graphite render API](https://docs.victoriametrics.com/#graphite-render-api-usage). Previously it was incorrectly returning timeseries with min peaks instead of max peaks. * BUGFIX: properly limit indexdb cache sizes. Previously they could exceed values set via `-memory.allowedPercent` and/or `-memory.allowedBytes` when `indexdb` contained many data parts. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2007). * BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix a bug, which could break time range picker when editing `From` or `To` input fields. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2080).