From 7b1c7051a37316ce2ebe10733416d0b92edf8822 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Thu, 13 Feb 2020 17:00:47 +0200 Subject: [PATCH] app/vmselect: add `sort_by_label(q, label)` and `sort_by_label_desc(q, label)` functions This is implementation of https://github.com/prometheus/prometheus/pull/1533 for VictoriaMetrics. --- app/vmselect/promql/exec.go | 3 ++- app/vmselect/promql/exec_test.go | 44 ++++++++++++++++++++++++++++++++ app/vmselect/promql/transform.go | 32 ++++++++++++++++++++--- docs/MetricsQL.md | 1 + lib/metricsql/transform.go | 2 ++ 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/app/vmselect/promql/exec.go b/app/vmselect/promql/exec.go index 1ea6fbfe7..87db688f3 100644 --- a/app/vmselect/promql/exec.go +++ b/app/vmselect/promql/exec.go @@ -71,7 +71,8 @@ func maySortResults(e metricsql.Expr, tss []*timeseries) bool { return true } switch fe.Name { - case "sort", "sort_desc": + case "sort", "sort_desc", + "sort_by_label", "sort_by_label_desc": return false default: return true diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go index 75a3b7e69..2b3f4e035 100644 --- a/app/vmselect/promql/exec_test.go +++ b/app/vmselect/promql/exec_test.go @@ -1597,6 +1597,48 @@ func TestExecSuccess(t *testing.T) { resultExpected := []netstorage.Result{r1, r2} f(q, resultExpected) }) + t.Run(`sort_by_label()`, func(t *testing.T) { + t.Parallel() + q := `sort_by_label(( + alias(1, "foo"), + alias(2, "bar"), + ), "__name__")` + r1 := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{2, 2, 2, 2, 2, 2}, + Timestamps: timestampsExpected, + } + r1.MetricName.MetricGroup = []byte("bar") + r2 := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{1, 1, 1, 1, 1, 1}, + Timestamps: timestampsExpected, + } + r2.MetricName.MetricGroup = []byte("foo") + resultExpected := []netstorage.Result{r1, r2} + f(q, resultExpected) + }) + t.Run(`sort_by_label_desc()`, func(t *testing.T) { + t.Parallel() + q := `sort_by_label_desc(( + alias(1, "foo"), + alias(2, "bar"), + ), "__name__")` + r1 := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{1, 1, 1, 1, 1, 1}, + Timestamps: timestampsExpected, + } + r1.MetricName.MetricGroup = []byte("foo") + r2 := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{2, 2, 2, 2, 2, 2}, + Timestamps: timestampsExpected, + } + r2.MetricName.MetricGroup = []byte("bar") + resultExpected := []netstorage.Result{r1, r2} + f(q, resultExpected) + }) t.Run(`a cmp scalar (leave MetricGroup)`, func(t *testing.T) { t.Parallel() q := `sort_desc(( @@ -5338,6 +5380,8 @@ func TestExecError(t *testing.T) { f(`scalar()`) f(`sort(1,2)`) f(`sort_desc()`) + f(`sort_by_label()`) + f(`sort_by_label_desc()`) f(`timestamp()`) f(`vector()`) f(`histogram_quantile()`) diff --git a/app/vmselect/promql/transform.go b/app/vmselect/promql/transform.go index f4f9acfa5..54c1dbf7e 100644 --- a/app/vmselect/promql/transform.go +++ b/app/vmselect/promql/transform.go @@ -97,6 +97,8 @@ var transformFuncs = map[string]transformFunc{ "acos": newTransformFuncOneArg(transformAcos), "prometheus_buckets": transformPrometheusBuckets, "histogram_share": transformHistogramShare, + "sort_by_label": newTransformFuncSortByLabel(false), + "sort_by_label_desc": newTransformFuncSortByLabel(true), } func getTransformFunc(s string) transformFunc { @@ -1355,6 +1357,29 @@ func transformScalar(tfa *transformFuncArg) ([]*timeseries, error) { return arg, nil } +func newTransformFuncSortByLabel(isDesc bool) transformFunc { + return func(tfa *transformFuncArg) ([]*timeseries, error) { + args := tfa.args + if err := expectTransformArgsNum(args, 2); err != nil { + return nil, err + } + label, err := getString(args[1], 1) + if err != nil { + return nil, fmt.Errorf("cannot parse label name for sorting: %s", err) + } + rvs := args[0] + sort.SliceStable(rvs, func(i, j int) bool { + a := rvs[i].MetricName.GetTagValue(label) + b := rvs[j].MetricName.GetTagValue(label) + if isDesc { + return string(b) < string(a) + } + return string(a) < string(b) + }) + return rvs, nil + } +} + func newTransformFuncSort(isDesc bool) transformFunc { return func(tfa *transformFuncArg) ([]*timeseries, error) { args := tfa.args @@ -1367,7 +1392,7 @@ func newTransformFuncSort(isDesc bool) transformFunc { b := rvs[j].Values n := len(a) - 1 for n >= 0 { - if !math.IsNaN(a[n]) && !math.IsNaN(b[n]) { + if !math.IsNaN(a[n]) && !math.IsNaN(b[n]) && a[n] != b[n] { break } n-- @@ -1375,11 +1400,10 @@ func newTransformFuncSort(isDesc bool) transformFunc { if n < 0 { return false } - cmp := a[n] < b[n] if isDesc { - cmp = !cmp + return b[n] < a[n] } - return cmp + return a[n] < b[n] }) return rvs, nil } diff --git a/docs/MetricsQL.md b/docs/MetricsQL.md index f79574a46..de4cfc506 100644 --- a/docs/MetricsQL.md +++ b/docs/MetricsQL.md @@ -53,6 +53,7 @@ This functionality can be tried at [an editable Grafana dashboard](http://play-g - `label_transform(q, label, regexp, replacement)` for replacing all the `regexp` occurences with `replacement` in the `label` values from `q`. - `label_value(q, label)` - returns numeric values for the given `label` from `q`. - `label_match(q, label, regexp)` and `label_mismatch(q, label, regexp)` for filtering time series with labels matching (or not matching) the given regexps. +- `sort_by_label(q, label)` and `sort_by_label_desc(q, label)` for sorting time series by the given `label`. - `step()` function for returning the step in seconds used in the query. - `start()` and `end()` functions for returning the start and end timestamps of the `[start ... end]` range used in the query. - `integrate(m[d])` for returning integral over the given duration `d` for the given metric `m`. diff --git a/lib/metricsql/transform.go b/lib/metricsql/transform.go index 53c262561..082eaeeb5 100644 --- a/lib/metricsql/transform.go +++ b/lib/metricsql/transform.go @@ -76,6 +76,8 @@ var transformFuncs = map[string]bool{ "acos": true, "prometheus_buckets": true, "histogram_share": true, + "sort_by_label": true, + "sort_by_label_desc": true, } // IsTransformFunc returns whether funcName is known transform function.