diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go index 679c9cc5dc..b18e510300 100644 --- a/app/vmselect/promql/exec_test.go +++ b/app/vmselect/promql/exec_test.go @@ -1847,6 +1847,45 @@ func TestExecSuccess(t *testing.T) { resultExpected := []netstorage.Result{r1, r2} f(q, resultExpected) }) + t.Run(`sort_by_label(multiple_labels)`, func(t *testing.T) { + t.Parallel() + q := `sort_by_label(( + label_set(1, "x", "b", "y", "aa"), + label_set(2, "x", "a", "y", "aa"), + ), "y", "x")` + r1 := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{2, 2, 2, 2, 2, 2}, + Timestamps: timestampsExpected, + } + r1.MetricName.Tags = []storage.Tag{ + { + Key: []byte("x"), + Value: []byte("a"), + }, + { + Key: []byte("y"), + Value: []byte("aa"), + }, + } + r2 := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{1, 1, 1, 1, 1, 1}, + Timestamps: timestampsExpected, + } + r2.MetricName.Tags = []storage.Tag{ + { + Key: []byte("x"), + Value: []byte("b"), + }, + { + Key: []byte("y"), + Value: []byte("aa"), + }, + } + resultExpected := []netstorage.Result{r1, r2} + f(q, resultExpected) + }) t.Run(`scalar < time()`, func(t *testing.T) { t.Parallel() q := `123 < time()` diff --git a/app/vmselect/promql/transform.go b/app/vmselect/promql/transform.go index 34ac4a6f7e..6d1b01cbdb 100644 --- a/app/vmselect/promql/transform.go +++ b/app/vmselect/promql/transform.go @@ -1603,21 +1603,31 @@ func transformScalar(tfa *transformFuncArg) ([]*timeseries, error) { func newTransformFuncSortByLabel(isDesc bool) transformFunc { return func(tfa *transformFuncArg) ([]*timeseries, error) { args := tfa.args - if err := expectTransformArgsNum(args, 2); err != nil { - return nil, err + if len(args) < 2 { + return nil, fmt.Errorf("expecting at least 2 args; got %d args", len(args)) } - label, err := getString(args[1], 1) - if err != nil { - return nil, fmt.Errorf("cannot parse label name for sorting: %w", err) + var labels []string + for i, arg := range args[1:] { + label, err := getString(arg, 1) + if err != nil { + return nil, fmt.Errorf("cannot parse label #%d for sorting: %w", i+1, err) + } + labels = append(labels, label) } 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) + for _, label := range labels { + a := rvs[i].MetricName.GetTagValue(label) + b := rvs[j].MetricName.GetTagValue(label) + if string(a) == string(b) { + continue + } + if isDesc { + return string(b) < string(a) + } + return string(a) < string(b) } - return string(a) < string(b) + return false }) return rvs, nil } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0387608a67..5f8ce23bc9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,7 @@ * FEATURE: provide a sample list of alerting rules for VictoriaMetrics components. It is available [here](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts.yml). * FEATURE: disable final merge for data for the previous month at the beginning of new month, since it may result in high disk IO and CPU usage. Final merge can be enabled by setting `-finalMergeDelay` command-line flag to positive duration. * FEATURE: add `tfirst_over_time(m[d])` and `tlast_over_time(m[d])` functions to [MetricsQL](https://victoriametrics.github.io/MetricsQL.html) for returning timestamps for the first and the last data point in `m` over `d` duration. +* FEATURE: add ability to pass multiple labels to `sort_by_label()` and `sort_by_label_desc()` functions. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/992 . * FEATURE: enforce at least TLS v1.2 when accepting HTTPS requests if `-tls`, `-tlsCertFile` and `-tlsKeyFile` command-line flags are set, because older TLS protocols such as v1.0 and v1.1 have been deprecated due to security vulnerabilities. * FEATURE: support `extra_label` query arg for all HTTP-based [data ingestion protocols](https://victoriametrics.github.io/#how-to-import-time-series-data). This query arg can be used for specifying extra labels which should be added for the ingested data. * FEATURE: vmbackup: increase backup chunk size from 128MB to 1GB. This should reduce the number of Object storage API calls during backups by 8x. This may also reduce costs, since object storage API calls usually have non-zero costs. See https://aws.amazon.com/s3/pricing/ and https://cloud.google.com/storage/pricing#operations-pricing . diff --git a/docs/MetricsQL.md b/docs/MetricsQL.md index b26c5f4f9e..474121c5dd 100644 --- a/docs/MetricsQL.md +++ b/docs/MetricsQL.md @@ -66,7 +66,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`. +- `sort_by_label(q, label1, ... labelN)` and `sort_by_label_desc(q, label1, ... labelN)` for sorting time series by the given set of labels. - `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`.