promql: use natural sort in sort_by_label and sort_by_label_desc (#13411)

These functions are intended for humans, as robots can already sort the results
however they please. Humans like things sorted "naturally":

* https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/

A similar thing has been done to Grafana, which is also used by humans:

* https://github.com/grafana/grafana/pull/78024
* https://github.com/grafana/grafana/pull/78494

Signed-off-by: Ivan Babrou <github@ivan.computer>
This commit is contained in:
Ivan Babrou 2024-01-16 16:34:09 -08:00 committed by GitHub
parent 7153f61790
commit a6b35ff304
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 56 additions and 15 deletions

View file

@ -594,6 +594,8 @@ Same as `sort`, but sorts in descending order.
Please note that the sort by label functions only affect the results of instant queries, as range query results always have a fixed output ordering. Please note that the sort by label functions only affect the results of instant queries, as range query results always have a fixed output ordering.
This function uses [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order).
## `sort_by_label_desc()` ## `sort_by_label_desc()`
**This function has to be enabled via the [feature flag](../feature_flags/) `--enable-feature=promql-experimental-functions`.** **This function has to be enabled via the [feature flag](../feature_flags/) `--enable-feature=promql-experimental-functions`.**
@ -602,6 +604,8 @@ Same as `sort_by_label`, but sorts in descending order.
Please note that the sort by label functions only affect the results of instant queries, as range query results always have a fixed output ordering. Please note that the sort by label functions only affect the results of instant queries, as range query results always have a fixed output ordering.
This function uses [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order).
## `sqrt()` ## `sqrt()`
`sqrt(v instant-vector)` calculates the square root of all elements in `v`. `sqrt(v instant-vector)` calculates the square root of all elements in `v`.

1
go.mod
View file

@ -19,6 +19,7 @@ require (
github.com/edsrzf/mmap-go v1.1.0 github.com/edsrzf/mmap-go v1.1.0
github.com/envoyproxy/go-control-plane v0.11.1 github.com/envoyproxy/go-control-plane v0.11.1
github.com/envoyproxy/protoc-gen-validate v1.0.2 github.com/envoyproxy/protoc-gen-validate v1.0.2
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.7.0
github.com/go-kit/log v0.2.1 github.com/go-kit/log v0.2.1
github.com/go-logfmt/logfmt v0.6.0 github.com/go-logfmt/logfmt v0.6.0

2
go.sum
View file

@ -166,6 +166,8 @@ github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBF
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=

View file

@ -21,6 +21,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/facette/natsort"
"github.com/grafana/regexp" "github.com/grafana/regexp"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@ -380,15 +381,16 @@ func funcSortByLabel(vals []parser.Value, args parser.Expressions, enh *EvalNode
for _, label := range labels { for _, label := range labels {
lv1 := a.Metric.Get(label) lv1 := a.Metric.Get(label)
lv2 := b.Metric.Get(label) lv2 := b.Metric.Get(label)
// 0 if a == b, -1 if a < b, and +1 if a > b.
switch strings.Compare(lv1, lv2) { if lv1 == lv2 {
case -1:
return -1
case +1:
return +1
default:
continue continue
} }
if natsort.Compare(lv1, lv2) {
return -1
}
return +1
} }
return 0 return 0
@ -409,19 +411,16 @@ func funcSortByLabelDesc(vals []parser.Value, args parser.Expressions, enh *Eval
for _, label := range labels { for _, label := range labels {
lv1 := a.Metric.Get(label) lv1 := a.Metric.Get(label)
lv2 := b.Metric.Get(label) lv2 := b.Metric.Get(label)
// If label values are the same, continue to the next label
if lv1 == lv2 { if lv1 == lv2 {
continue continue
} }
// 0 if a == b, -1 if a < b, and +1 if a > b.
switch strings.Compare(lv1, lv2) { if natsort.Compare(lv1, lv2) {
case -1:
return +1 return +1
case +1:
return -1
default:
continue
} }
return -1
} }
return 0 return 0

View file

@ -482,6 +482,19 @@ load 5m
http_requests{job="app-server", instance="0", group="canary"} 0+70x10 http_requests{job="app-server", instance="0", group="canary"} 0+70x10
http_requests{job="app-server", instance="1", group="canary"} 0+80x10 http_requests{job="app-server", instance="1", group="canary"} 0+80x10
http_requests{job="api-server", instance="2", group="production"} 0+10x10 http_requests{job="api-server", instance="2", group="production"} 0+10x10
cpu_time_total{job="cpu", cpu="0"} 0+10x10
cpu_time_total{job="cpu", cpu="1"} 0+10x10
cpu_time_total{job="cpu", cpu="2"} 0+10x10
cpu_time_total{job="cpu", cpu="3"} 0+10x10
cpu_time_total{job="cpu", cpu="10"} 0+10x10
cpu_time_total{job="cpu", cpu="11"} 0+10x10
cpu_time_total{job="cpu", cpu="12"} 0+10x10
cpu_time_total{job="cpu", cpu="20"} 0+10x10
cpu_time_total{job="cpu", cpu="21"} 0+10x10
cpu_time_total{job="cpu", cpu="100"} 0+10x10
node_uname_info{job="node_exporter", instance="4m600", release="1.2.3"} 0+10x10
node_uname_info{job="node_exporter", instance="4m5", release="1.11.3"} 0+10x10
node_uname_info{job="node_exporter", instance="4m1000", release="1.111.3"} 0+10x10
eval_ordered instant at 50m sort_by_label(http_requests, "instance") eval_ordered instant at 50m sort_by_label(http_requests, "instance")
http_requests{group="production", instance="0", job="api-server"} 100 http_requests{group="production", instance="0", job="api-server"} 100
@ -579,6 +592,28 @@ eval_ordered instant at 50m sort_by_label_desc(http_requests, "instance", "group
http_requests{group="canary", instance="0", job="app-server"} 700 http_requests{group="canary", instance="0", job="app-server"} 700
http_requests{group="canary", instance="0", job="api-server"} 300 http_requests{group="canary", instance="0", job="api-server"} 300
eval_ordered instant at 50m sort_by_label(cpu_time_total, "cpu")
cpu_time_total{job="cpu", cpu="0"} 100
cpu_time_total{job="cpu", cpu="1"} 100
cpu_time_total{job="cpu", cpu="2"} 100
cpu_time_total{job="cpu", cpu="3"} 100
cpu_time_total{job="cpu", cpu="10"} 100
cpu_time_total{job="cpu", cpu="11"} 100
cpu_time_total{job="cpu", cpu="12"} 100
cpu_time_total{job="cpu", cpu="20"} 100
cpu_time_total{job="cpu", cpu="21"} 100
cpu_time_total{job="cpu", cpu="100"} 100
eval_ordered instant at 50m sort_by_label(node_uname_info, "instance")
node_uname_info{job="node_exporter", instance="4m5", release="1.11.3"} 100
node_uname_info{job="node_exporter", instance="4m600", release="1.2.3"} 100
node_uname_info{job="node_exporter", instance="4m1000", release="1.111.3"} 100
eval_ordered instant at 50m sort_by_label(node_uname_info, "release")
node_uname_info{job="node_exporter", instance="4m600", release="1.2.3"} 100
node_uname_info{job="node_exporter", instance="4m5", release="1.11.3"} 100
node_uname_info{job="node_exporter", instance="4m1000", release="1.111.3"} 100
# Tests for holt_winters # Tests for holt_winters
clear clear