diff --git a/pkg/promlib/models/scope.go b/pkg/promlib/models/scope.go index 41fbbb43444..767a98a854e 100644 --- a/pkg/promlib/models/scope.go +++ b/pkg/promlib/models/scope.go @@ -4,10 +4,15 @@ import ( "fmt" "strings" + "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" ) +func init() { + model.NameValidationScheme = model.UTF8Validation +} + // ApplyFiltersAndGroupBy takes a raw promQL expression, converts the filters into PromQL matchers, and applies these matchers to the parsed expression. It also applies the group by clause to any aggregate expressions in the parsed expression. func ApplyFiltersAndGroupBy(rawExpr string, scopeFilters, adHocFilters []ScopeFilter, groupBy []string) (string, error) { expr, err := parser.ParseExpr(rawExpr) diff --git a/pkg/promlib/models/scope_test.go b/pkg/promlib/models/scope_test.go index fc7d972eb44..fb6d5f4c1a3 100644 --- a/pkg/promlib/models/scope_test.go +++ b/pkg/promlib/models/scope_test.go @@ -145,6 +145,37 @@ func TestApplyQueryFiltersAndGroupBy_Filters(t *testing.T) { } } +func TestApplyQueryFiltersAndGroupBy_Filters_utf8(t *testing.T) { + tests := []struct { + name string + query string + adhocFilters []ScopeFilter + scopeFilters []ScopeFilter + expected string + expectErr bool + }{ + { + name: "No filters with existing utf8 filter", + query: `http_requests_total{"job.name"="prometheus"}`, + expected: `http_requests_total{"job.name"="prometheus"}`, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expr, err := ApplyFiltersAndGroupBy(tt.query, tt.scopeFilters, tt.adhocFilters, nil) + + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, expr, tt.name) + } + }) + } +} + func TestApplyQueryFiltersAndGroupBy_GroupBy(t *testing.T) { tests := []struct { name string @@ -197,7 +228,45 @@ func TestApplyQueryFiltersAndGroupBy_GroupBy(t *testing.T) { require.Error(t, err) } else { require.NoError(t, err) - require.Equal(t, expr, tt.expected) + require.Equal(t, tt.expected, expr) + } + }) + } +} + +func TestApplyQueryFiltersAndGroupBy_GroupBy_utf8(t *testing.T) { + tests := []struct { + name string + query string + groupBy []string + expected string + expectErr bool + }{ + { + name: "GroupBy with no aggregate expression and utf8 metric", + groupBy: []string{"job"}, + query: `{"http.requests_total"}`, + expected: `{__name__="http.requests_total"}`, + expectErr: false, + }, + { + name: "GroupBy with aggregate expression with existing utf8 group by", + groupBy: []string{"status"}, + query: `sum by ("utf8.job") (http_requests_total)`, + expected: `sum by ("utf8.job", status) (http_requests_total)`, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expr, err := ApplyFiltersAndGroupBy(tt.query, nil, nil, tt.groupBy) + + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, expr) } }) } @@ -237,7 +306,72 @@ func TestApplyQueryFiltersAndGroupBy(t *testing.T) { require.Error(t, err) } else { require.NoError(t, err) - require.Equal(t, expr, tt.expected) + require.Equal(t, tt.expected, expr) + } + }) + } +} + +func TestApplyQueryFiltersAndGroupBy_utf8(t *testing.T) { + tests := []struct { + name string + query string + adhocFilters []ScopeFilter + scopeFilters []ScopeFilter + groupby []string + expected string + expectErr bool + }{ + { + name: "Adhoc filters with more complex expression and utf8 metric name", + query: `sum({"capacity_bytes", job="prometheus"} + {"available_bytes", job="grafana"}) / 1024`, + adhocFilters: []ScopeFilter{ + {Key: "job", Value: "alloy", Operator: FilterOperatorEquals}, + }, + scopeFilters: []ScopeFilter{ + {Key: "vol", Value: "/", Operator: FilterOperatorEquals}, + }, + groupby: []string{"job"}, + expected: `sum by (job) ({__name__="capacity_bytes",job="alloy",vol="/"} + {__name__="available_bytes",job="alloy",vol="/"}) / 1024`, + expectErr: false, + }, + { + name: "Adhoc filters with more complex expression with utf8 label", + query: `sum(capacity_bytes{job="prometheus", "utf8.label"="value"} + available_bytes{job="grafana"}) / 1024`, + adhocFilters: []ScopeFilter{ + {Key: "job", Value: "alloy", Operator: FilterOperatorEquals}, + }, + scopeFilters: []ScopeFilter{ + {Key: "vol", Value: "/", Operator: FilterOperatorEquals}, + }, + groupby: []string{"job"}, + expected: `sum by (job) (capacity_bytes{"utf8.label"="value",job="alloy",vol="/"} + available_bytes{job="alloy",vol="/"}) / 1024`, + expectErr: false, + }, + { + name: "Adhoc filters with more complex expression with utf8 metric and label", + query: `sum({"capacity_bytes", job="prometheus", "utf8.label"="value"} + available_bytes{job="grafana"}) / 1024`, + adhocFilters: []ScopeFilter{ + {Key: "job", Value: "alloy", Operator: FilterOperatorEquals}, + }, + scopeFilters: []ScopeFilter{ + {Key: "vol", Value: "/", Operator: FilterOperatorEquals}, + }, + groupby: []string{"job"}, + expected: `sum by (job) ({"utf8.label"="value",__name__="capacity_bytes",job="alloy",vol="/"} + available_bytes{job="alloy",vol="/"}) / 1024`, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expr, err := ApplyFiltersAndGroupBy(tt.query, tt.scopeFilters, tt.adhocFilters, tt.groupby) + + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, expr) } }) } diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 2ccfd9ee5f1..dd55c5df488 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -785,15 +785,6 @@ func TestIntegrationInsertAlertRules(t *testing.T) { require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) require.ErrorContains(t, err, "metric name for recording rule must be a valid utf8 string") }) - - t.Run("invalid metric name", func(t *testing.T) { - invalidMetric := "with-dashes" - invalidRule := recordingRulesGen.Generate() - invalidRule.Record.Metric = invalidMetric - _, err := store.InsertAlertRules(context.Background(), []models.AlertRule{invalidRule}) - require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) - require.ErrorContains(t, err, "metric name for recording rule must be a valid Prometheus metric name") - }) }) t.Run("clears fields that should not exist on recording rules", func(t *testing.T) {