From a6b35ff304ca22c8676d8da244eb837f04f64169 Mon Sep 17 00:00:00 2001 From: Ivan Babrou Date: Tue, 16 Jan 2024 16:34:09 -0800 Subject: [PATCH] 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 --- docs/querying/functions.md | 4 ++++ go.mod | 1 + go.sum | 2 ++ promql/functions.go | 29 ++++++++++++++-------------- promql/testdata/functions.test | 35 ++++++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/docs/querying/functions.md b/docs/querying/functions.md index dda88fccd..607e16e82 100644 --- a/docs/querying/functions.md +++ b/docs/querying/functions.md @@ -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. +This function uses [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order). + ## `sort_by_label_desc()` **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. +This function uses [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order). + ## `sqrt()` `sqrt(v instant-vector)` calculates the square root of all elements in `v`. diff --git a/go.mod b/go.mod index 428b6edec..e7598db42 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/edsrzf/mmap-go v1.1.0 github.com/envoyproxy/go-control-plane v0.11.1 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/go-kit/log v0.2.1 github.com/go-logfmt/logfmt v0.6.0 diff --git a/go.sum b/go.sum index 8ce4efb5c..fa21d23e5 100644 --- a/go.sum +++ b/go.sum @@ -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/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/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.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= diff --git a/promql/functions.go b/promql/functions.go index 407a11b50..cb3be0aef 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -21,6 +21,7 @@ import ( "strings" "time" + "github.com/facette/natsort" "github.com/grafana/regexp" "github.com/prometheus/common/model" "golang.org/x/exp/slices" @@ -380,15 +381,16 @@ func funcSortByLabel(vals []parser.Value, args parser.Expressions, enh *EvalNode for _, label := range labels { lv1 := a.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) { - case -1: - return -1 - case +1: - return +1 - default: + + if lv1 == lv2 { continue } + + if natsort.Compare(lv1, lv2) { + return -1 + } + + return +1 } return 0 @@ -409,19 +411,16 @@ func funcSortByLabelDesc(vals []parser.Value, args parser.Expressions, enh *Eval for _, label := range labels { lv1 := a.Metric.Get(label) lv2 := b.Metric.Get(label) - // If label values are the same, continue to the next label + if lv1 == lv2 { continue } - // 0 if a == b, -1 if a < b, and +1 if a > b. - switch strings.Compare(lv1, lv2) { - case -1: + + if natsort.Compare(lv1, lv2) { return +1 - case +1: - return -1 - default: - continue } + + return -1 } return 0 diff --git a/promql/testdata/functions.test b/promql/testdata/functions.test index b4547886a..c40a6272b 100644 --- a/promql/testdata/functions.test +++ b/promql/testdata/functions.test @@ -482,6 +482,19 @@ load 5m 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="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") 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="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 clear