Skip to content

Commit a95ee0f

Browse files
committed
Add function to analyse used labels, and not just used metrics.
Signed-off-by: Tom Wilkie <[email protected]>
1 parent 3048e55 commit a95ee0f

File tree

3 files changed

+183
-0
lines changed

3 files changed

+183
-0
lines changed

pkg/commands/analyse.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package commands
22

33
import (
4+
"encoding/json"
5+
"os"
6+
7+
log "github.com/sirupsen/logrus"
48
"gopkg.in/alecthomas/kingpin.v2"
59
)
610

@@ -91,4 +95,12 @@ func (cmd *AnalyseCommand) Register(app *kingpin.Application) {
9195
ruleFileAnalyseCmd.Flag("output", "The path for the output file").
9296
Default("metrics-in-ruler.json").
9397
StringVar(&rfCmd.outputFile)
98+
99+
analyseCmd.Command("queries", "Extract the used metrics and labels from queries fed in on stdin.").Action(func(_ *kingpin.ParseContext) error {
100+
metrics, err := processQueries(os.Stdin)
101+
if err != nil {
102+
log.Fatalf("failed to process queries: %v", err)
103+
}
104+
return json.NewEncoder(os.Stdout).Encode(metrics)
105+
})
94106
}

pkg/commands/analyse_queries.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package commands
2+
3+
import (
4+
"bufio"
5+
"io"
6+
"sort"
7+
8+
"github.com/prometheus/prometheus/pkg/labels"
9+
"github.com/prometheus/prometheus/promql/parser"
10+
)
11+
12+
type MetricUsage struct {
13+
LabelsUsed []string
14+
}
15+
16+
func processQueries(r io.Reader) (map[string]MetricUsage, error) {
17+
metrics := map[string]MetricUsage{}
18+
scanner := bufio.NewScanner(r)
19+
for scanner.Scan() {
20+
if err := processQuery(scanner.Text(), metrics); err != nil {
21+
return nil, err
22+
}
23+
}
24+
25+
return metrics, scanner.Err()
26+
}
27+
28+
func processQuery(query string, metrics map[string]MetricUsage) error {
29+
expr, err := parser.ParseExpr(query)
30+
if err != nil {
31+
return err
32+
}
33+
34+
parser.Inspect(expr, func(node parser.Node, path []parser.Node) error {
35+
vs, ok := node.(*parser.VectorSelector)
36+
if !ok {
37+
return nil
38+
}
39+
40+
metricName, ok := getName(vs.LabelMatchers)
41+
if !ok {
42+
return nil
43+
}
44+
45+
usedLabels := metrics[metricName]
46+
47+
// Add any label names from the selectors to the list of used labels.
48+
for _, matcher := range vs.LabelMatchers {
49+
if matcher.Name == labels.MetricName {
50+
continue
51+
}
52+
setInsert(matcher.Name, &usedLabels.LabelsUsed)
53+
}
54+
55+
// Find any aggregations in the path and add grouping labels.
56+
for _, node := range path {
57+
ae, ok := node.(*parser.AggregateExpr)
58+
if !ok {
59+
continue
60+
}
61+
62+
for _, label := range ae.Grouping {
63+
setInsert(label, &usedLabels.LabelsUsed)
64+
}
65+
}
66+
metrics[metricName] = usedLabels
67+
68+
return nil
69+
})
70+
71+
return nil
72+
}
73+
74+
func getName(matchers []*labels.Matcher) (string, bool) {
75+
for _, matcher := range matchers {
76+
if matcher.Name == labels.MetricName && matcher.Type == labels.MatchEqual {
77+
return matcher.Value, true
78+
}
79+
}
80+
return "", false
81+
}
82+
83+
func setInsert(label string, labels *[]string) {
84+
i := sort.Search(len(*labels), func(i int) bool { return (*labels)[i] >= label })
85+
if i < len(*labels) && (*labels)[i] == label {
86+
// label is present at labels[i]
87+
return
88+
}
89+
90+
// label is not present in labels,
91+
// but i is the index where it would be inserted.
92+
*labels = append(*labels, "")
93+
copy((*labels)[i+1:], (*labels)[i:])
94+
(*labels)[i] = label
95+
}

pkg/commands/analyse_queries_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package commands
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestSetInsert(t *testing.T) {
10+
for _, tc := range []struct {
11+
initial []string
12+
value string
13+
expected []string
14+
}{
15+
{
16+
initial: []string{},
17+
value: "foo",
18+
expected: []string{"foo"},
19+
},
20+
{
21+
initial: []string{"foo"},
22+
value: "foo",
23+
expected: []string{"foo"},
24+
},
25+
{
26+
initial: []string{"foo"},
27+
value: "bar",
28+
expected: []string{"bar", "foo"},
29+
},
30+
{
31+
initial: []string{"bar"},
32+
value: "foo",
33+
expected: []string{"bar", "foo"},
34+
},
35+
{
36+
initial: []string{"bar", "foo"},
37+
value: "bar",
38+
expected: []string{"bar", "foo"},
39+
},
40+
} {
41+
setInsert(tc.value, &tc.initial)
42+
require.Equal(t, tc.initial, tc.expected)
43+
}
44+
}
45+
46+
func TestProcessQuery(t *testing.T) {
47+
for _, tc := range []struct {
48+
query string
49+
expected map[string]MetricUsage
50+
}{
51+
{
52+
query: `sum(rate(requests_total{status=~"5.."}[5m])) / sum(rate(requests_total[5m]))`,
53+
expected: map[string]MetricUsage{
54+
"requests_total": {LabelsUsed: []string{"status"}},
55+
},
56+
},
57+
{
58+
query: `sum(rate(requests_sum[5m])) / sum(rate(requests_total[5m]))`,
59+
expected: map[string]MetricUsage{
60+
"requests_total": {LabelsUsed: nil},
61+
"requests_sum": {LabelsUsed: nil},
62+
},
63+
},
64+
{
65+
query: `sum by (path) (rate(requests_total{status=~"5.."}[5m]))`,
66+
expected: map[string]MetricUsage{
67+
"requests_total": {LabelsUsed: []string{"path", "status"}},
68+
},
69+
},
70+
} {
71+
actual := map[string]MetricUsage{}
72+
err := processQuery(tc.query, actual)
73+
require.NoError(t, err)
74+
require.Equal(t, tc.expected, actual)
75+
}
76+
}

0 commit comments

Comments
 (0)