Skip to content

Commit e6daf27

Browse files
authored
expose an analyze package so anybody can analyze metrics usage (#23)
Signed-off-by: Augustin Husson <[email protected]>
1 parent 11cafbb commit e6daf27

File tree

15 files changed

+631
-485
lines changed

15 files changed

+631
-485
lines changed

pkg/analyze/grafana/grafana.go

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
// Copyright 2024 The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package grafana
15+
16+
import (
17+
"fmt"
18+
"regexp"
19+
"strings"
20+
21+
"github.com/perses/metrics-usage/pkg/analyze/prometheus"
22+
modelAPIV1 "github.com/perses/metrics-usage/pkg/api/v1"
23+
"github.com/perses/metrics-usage/utils"
24+
)
25+
26+
type variableTuple struct {
27+
name string
28+
value string
29+
}
30+
31+
var (
32+
labelValuesRegexp = regexp.MustCompile(`(?s)label_values\((.+),.+\)`)
33+
labelValuesNoQueryRegexp = regexp.MustCompile(`(?s)label_values\((.+)\)`)
34+
queryResultRegexp = regexp.MustCompile(`(?s)query_result\((.+)\)`)
35+
variableRangeQueryRangeRegex = regexp.MustCompile(`\[\$?\w+?]`)
36+
variableSubqueryRangeRegex = regexp.MustCompile(`\[\$?\w+:\$?\w+?]`)
37+
globalVariableList = []variableTuple{
38+
// Don't change the order.
39+
// The order matters because, when replacing the variable with its value in the expression, if, for example,
40+
// __interval is replaced before __interval_ms, then you might have partially replaced the variable.
41+
// Example: 1 / __interval_ms will give 1 / 20m_s which is not a correct PromQL expression.
42+
// So we need to replace __interval_ms before __interval.
43+
// Same thing applied for every variable starting with the same prefix. Like __from, __to.
44+
{
45+
name: "__interval_ms",
46+
value: "1200000",
47+
},
48+
{
49+
name: "__interval",
50+
value: "20m",
51+
},
52+
{
53+
name: "interval",
54+
value: "20m",
55+
},
56+
{
57+
name: "resolution",
58+
value: "5m",
59+
},
60+
{
61+
name: "__rate_interval_ms",
62+
value: "1200000",
63+
},
64+
{
65+
name: "__rate_interval",
66+
value: "20m",
67+
},
68+
{
69+
name: "rate_interval",
70+
value: "20m",
71+
},
72+
{
73+
name: "__range_s:glob",
74+
value: "15",
75+
},
76+
{
77+
name: "__range_s",
78+
value: "15",
79+
},
80+
{
81+
name: "__range_ms",
82+
value: "15",
83+
},
84+
{
85+
name: "__range",
86+
value: "1d",
87+
},
88+
{
89+
name: "__from:date:YYYY-MM",
90+
value: "2020-07",
91+
},
92+
{
93+
name: "__from:date:seconds",
94+
value: "1594671549",
95+
},
96+
{
97+
name: "__from:date:iso",
98+
value: "2020-07-13T20:19:09.254Z",
99+
},
100+
{
101+
name: "__from:date",
102+
value: "2020-07-13T20:19:09.254Z",
103+
},
104+
{
105+
name: "__from",
106+
value: "1594671549254",
107+
},
108+
{
109+
name: "__to:date:YYYY-MM",
110+
value: "2020-07",
111+
},
112+
{
113+
name: "__to:date:seconds",
114+
value: "1594671549",
115+
},
116+
{
117+
name: "__to:date:iso",
118+
value: "2020-07-13T20:19:09.254Z",
119+
},
120+
{
121+
name: "__to:date",
122+
value: "2020-07-13T20:19:09.254Z",
123+
},
124+
{
125+
name: "__to",
126+
value: "1594671549254",
127+
},
128+
{
129+
name: "__user",
130+
value: "foo",
131+
},
132+
{
133+
name: "__org",
134+
value: "perses",
135+
},
136+
{
137+
name: "__name",
138+
value: "john",
139+
},
140+
{
141+
name: "__dashboard",
142+
value: "the_infamous_one",
143+
},
144+
}
145+
variableReplacer = strings.NewReplacer(generateGrafanaTupleVariableSyntaxReplacer(globalVariableList)...)
146+
)
147+
148+
func Analyze(dashboard *SimplifiedDashboard) ([]string, []*modelAPIV1.LogError) {
149+
staticVariables := strings.NewReplacer(generateGrafanaVariableSyntaxReplacer(extractStaticVariables(dashboard.Templating.List))...)
150+
m1, err1 := extractMetricsFromPanels(dashboard.Panels, staticVariables, dashboard)
151+
for _, r := range dashboard.Rows {
152+
m2, err2 := extractMetricsFromPanels(r.Panels, staticVariables, dashboard)
153+
m1 = utils.Merge(m1, m2)
154+
err1 = append(err1, err2...)
155+
}
156+
m3, err3 := extractMetricsFromVariables(dashboard.Templating.List, staticVariables, dashboard)
157+
return utils.Merge(m1, m3), append(err1, err3...)
158+
}
159+
160+
func extractMetricsFromPanels(panels []Panel, staticVariables *strings.Replacer, dashboard *SimplifiedDashboard) ([]string, []*modelAPIV1.LogError) {
161+
var errs []*modelAPIV1.LogError
162+
var result []string
163+
for _, p := range panels {
164+
for _, t := range extractTarget(p) {
165+
if len(t.Expr) == 0 {
166+
continue
167+
}
168+
metrics, err := prometheus.AnalyzePromQLExpression(replaceVariables(t.Expr, staticVariables))
169+
if err != nil {
170+
errs = append(errs, &modelAPIV1.LogError{
171+
Error: err,
172+
Message: fmt.Sprintf("failed to extract metric names from PromQL expression in the panel %q for the dashboard %s/%s", p.Title, dashboard.Title, dashboard.UID),
173+
})
174+
} else {
175+
result = utils.Merge(result, metrics)
176+
}
177+
}
178+
}
179+
return result, errs
180+
}
181+
182+
func extractMetricsFromVariables(variables []templateVar, staticVariables *strings.Replacer, dashboard *SimplifiedDashboard) ([]string, []*modelAPIV1.LogError) {
183+
var errs []*modelAPIV1.LogError
184+
var result []string
185+
for _, v := range variables {
186+
if v.Type != "query" {
187+
continue
188+
}
189+
query, err := v.extractQueryFromVariableTemplating()
190+
if err != nil {
191+
// It appears when there is an issue, we cannot do anything about it actually and usually the variable is not the one we are looking for.
192+
// So we just log it as a warning
193+
errs = append(errs, &modelAPIV1.LogError{
194+
Warning: err,
195+
Message: fmt.Sprintf("failed to extract query in variable %q for the dashboard %s/%s", v.Name, dashboard.Title, dashboard.UID),
196+
})
197+
continue
198+
}
199+
// label_values(query, label)
200+
if labelValuesRegexp.MatchString(query) {
201+
sm := labelValuesRegexp.FindStringSubmatch(query)
202+
if len(sm) > 0 {
203+
query = sm[1]
204+
} else {
205+
continue
206+
}
207+
} else if labelValuesNoQueryRegexp.MatchString(query) {
208+
// No query so no metric.
209+
continue
210+
} else if queryResultRegexp.MatchString(query) {
211+
// query_result(query)
212+
query = queryResultRegexp.FindStringSubmatch(query)[1]
213+
}
214+
metrics, err := prometheus.AnalyzePromQLExpression(replaceVariables(query, staticVariables))
215+
if err != nil {
216+
errs = append(errs, &modelAPIV1.LogError{
217+
Error: err,
218+
Message: fmt.Sprintf("failed to extract metric names from PromQL expression in variable %q for the dashboard %s/%s", v.Name, dashboard.Title, dashboard.UID),
219+
})
220+
} else {
221+
result = utils.Merge(result, metrics)
222+
}
223+
}
224+
return result, errs
225+
}
226+
227+
func extractStaticVariables(variables []templateVar) map[string]string {
228+
result := make(map[string]string)
229+
for _, v := range variables {
230+
if v.Type == "query" {
231+
// We don't want to look at the runtime query. We are using them to extract metrics instead.
232+
continue
233+
}
234+
if len(v.Options) > 0 {
235+
result[v.Name] = v.Options[0].Value
236+
}
237+
}
238+
return result
239+
}
240+
241+
func replaceVariables(expr string, staticVariables *strings.Replacer) string {
242+
newExpr := staticVariables.Replace(expr)
243+
newExpr = variableReplacer.Replace(newExpr)
244+
newExpr = variableRangeQueryRangeRegex.ReplaceAllLiteralString(newExpr, `[5m]`)
245+
newExpr = variableSubqueryRangeRegex.ReplaceAllLiteralString(newExpr, `[5m:1m]`)
246+
return newExpr
247+
}
248+
249+
func generateGrafanaVariableSyntaxReplacer(variables map[string]string) []string {
250+
var result []string
251+
for variable, value := range variables {
252+
result = append(result, fmt.Sprintf("$%s", variable), value, fmt.Sprintf("${%s}", variable), value)
253+
}
254+
return result
255+
}
256+
257+
func generateGrafanaTupleVariableSyntaxReplacer(variables []variableTuple) []string {
258+
var result []string
259+
for _, v := range variables {
260+
result = append(result, fmt.Sprintf("$%s", v.name), v.value, fmt.Sprintf("${%s}", v.name), v.value)
261+
}
262+
return result
263+
}

source/grafana/grafana_test.go renamed to pkg/analyze/grafana/grafana_test.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,42 @@
1+
// Copyright 2024 The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
114
package grafana
215

316
import (
417
"encoding/json"
518
"os"
619
"testing"
720

21+
modelAPIV1 "github.com/perses/metrics-usage/pkg/api/v1"
822
"github.com/stretchr/testify/assert"
923
)
1024

11-
func unmarshalDashboard(path string) (*simplifiedDashboard, error) {
25+
func unmarshalDashboard(path string) (*SimplifiedDashboard, error) {
1226
data, err := os.ReadFile(path)
1327
if err != nil {
1428
return nil, err
1529
}
16-
result := &simplifiedDashboard{}
30+
result := &SimplifiedDashboard{}
1731
return result, json.Unmarshal(data, result)
1832
}
1933

20-
func TestExtractMetrics(t *testing.T) {
34+
func TestAnalyze(t *testing.T) {
2135
tests := []struct {
2236
name string
2337
dashboardFile string
2438
resultMetrics []string
25-
resultErrs []logError
39+
resultErrs []*modelAPIV1.LogError
2640
}{
2741
{
2842
name: "from/to variables",
@@ -46,7 +60,7 @@ func TestExtractMetrics(t *testing.T) {
4660
if err != nil {
4761
t.Fatal(err)
4862
}
49-
metrics, errs := extractMetrics(dashboard)
63+
metrics, errs := Analyze(dashboard)
5064
assert.Equal(t, tt.resultMetrics, metrics)
5165
assert.Equal(t, tt.resultErrs, errs)
5266
})

source/grafana/model.go renamed to pkg/analyze/grafana/model.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ package grafana
1515

1616
import "fmt"
1717

18-
type target struct {
18+
type Target struct {
1919
Expr string `json:"expr,omitempty"`
2020
}
2121

22-
type panel struct {
22+
type Panel struct {
2323
Type string `json:"type"`
2424
Title string `json:"title"`
25-
Panels []panel `json:"panels"`
26-
Targets []target `json:"targets"`
25+
Panels []Panel `json:"panels"`
26+
Targets []Target `json:"targets"`
2727
}
2828

2929
type row struct {
30-
Panels []panel `json:"panels"`
30+
Panels []Panel `json:"panels"`
3131
}
3232

3333
type option struct {
@@ -58,18 +58,18 @@ func (v templateVar) extractQueryFromVariableTemplating() (string, error) {
5858
return "", fmt.Errorf("unable to extract the query expression from the variable %q", v.Name)
5959
}
6060

61-
type simplifiedDashboard struct {
61+
type SimplifiedDashboard struct {
6262
UID string `json:"uid,omitempty"`
6363
Title string `json:"title"`
64-
Panels []panel `json:"panels"`
64+
Panels []Panel `json:"panels"`
6565
Rows []row `json:"rows"`
6666
Templating struct {
6767
List []templateVar `json:"list"`
6868
} `json:"templating"`
6969
}
7070

71-
func extractTarget(panel panel) []target {
72-
var targets []target
71+
func extractTarget(panel Panel) []Target {
72+
var targets []Target
7373
for _, p := range panel.Panels {
7474
targets = append(targets, extractTarget(p)...)
7575
}
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)