From a93a05aebd980b2a4aa7b0c7cf426ef3dc850409 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 6 Mar 2025 17:20:33 +0100 Subject: [PATCH] Alerting: Support group-level labels in Prometheus to Grafana conversion (#101624) --- pkg/services/ngalert/prom/convert.go | 16 +-- pkg/services/ngalert/prom/convert_test.go | 136 ++++++++++++++++++++-- pkg/services/ngalert/prom/models.go | 4 - pkg/services/ngalert/prom/models_test.go | 13 +-- 4 files changed, 140 insertions(+), 29 deletions(-) diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index d5460a83e26..56c6e4aa96c 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -2,6 +2,7 @@ package prom import ( "fmt" + "maps" "time" "github.com/google/uuid" @@ -120,7 +121,7 @@ func (p *Converter) convertRuleGroup(orgID int64, namespaceUID string, promGroup } for i, rule := range promGroup.Rules { - gr, err := p.convertRule(orgID, namespaceUID, promGroup.Name, rule) + gr, err := p.convertRule(orgID, namespaceUID, promGroup, rule) if err != nil { return nil, fmt.Errorf("failed to convert Prometheus rule '%s' to Grafana rule: %w", rule.Alert, err) } @@ -170,7 +171,7 @@ func getUID(orgID int64, namespaceUID string, group string, position int, promRu return u.String(), nil } -func (p *Converter) convertRule(orgID int64, namespaceUID, group string, rule PrometheusRule) (models.AlertRule, error) { +func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup PrometheusRuleGroup, rule PrometheusRule) (models.AlertRule, error) { var forInterval time.Duration if rule.For != nil { forInterval = time.Duration(*rule.For) @@ -206,12 +207,11 @@ func (p *Converter) convertRule(orgID int64, namespaceUID, group string, rule Pr // but Prometheus allows multiple rules with the same name. By adding the group name // to the title we ensure that the title is unique within the group. // TODO: Remove this workaround when we have a proper solution for handling rule title uniqueness. - title = fmt.Sprintf("[%s] %s", group, title) + title = fmt.Sprintf("[%s] %s", promGroup.Name, title) - labels := make(map[string]string, len(rule.Labels)+1) - for k, v := range rule.Labels { - labels[k] = v - } + labels := make(map[string]string, len(rule.Labels)+len(promGroup.Labels)) + maps.Copy(labels, promGroup.Labels) + maps.Copy(labels, rule.Labels) originalRuleDefinition, err := yaml.Marshal(rule) if err != nil { @@ -229,7 +229,7 @@ func (p *Converter) convertRule(orgID int64, namespaceUID, group string, rule Pr Annotations: rule.Annotations, Labels: labels, For: forInterval, - RuleGroup: group, + RuleGroup: promGroup.Name, IsPaused: isPaused, Record: record, Metadata: models.AlertRuleMetadata{ diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index a6af11d1cc9..3365ba15e5c 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -3,6 +3,7 @@ package prom import ( "encoding/json" "fmt" + "maps" "testing" "time" @@ -142,7 +143,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) { errorMsg: "limit is not supported", }, { - name: "rule group with labels is not supported", + name: "rule group with labels", orgID: 1, namespace: "namespaceUID", promGroup: PrometheusRuleGroup{ @@ -156,8 +157,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) { }, }, }, - expectError: true, - errorMsg: "labels are not supported", + expectError: false, }, } @@ -208,10 +208,9 @@ func TestPrometheusRulesToGrafana(t *testing.T) { } require.Equal(t, expectedFor, grafanaRule.For, tc.name) - expectedLabels := make(map[string]string, len(promRule.Labels)+1) - for k, v := range promRule.Labels { - expectedLabels[k] = v - } + expectedLabels := make(map[string]string, len(promRule.Labels)+len(tc.promGroup.Labels)) + maps.Copy(expectedLabels, tc.promGroup.Labels) + maps.Copy(expectedLabels, promRule.Labels) uidData := fmt.Sprintf("%d|%s|%s|%d", tc.orgID, tc.namespace, tc.promGroup.Name, j) u := uuid.NewSHA1(uuid.NameSpaceOID, []byte(uidData)) @@ -390,6 +389,129 @@ func TestPrometheusRulesToGrafana_NodesInRules(t *testing.T) { }) } +func TestPrometheusRulesToGrafana_GroupLabels(t *testing.T) { + cfg := Config{ + DatasourceUID: "datasource-uid", + DatasourceType: datasources.DS_PROMETHEUS, + DefaultInterval: 2 * time.Minute, + } + converter, err := NewConverter(cfg) + require.NoError(t, err) + + t.Run("group labels are merged with alert rule labels", func(t *testing.T) { + promGroup := PrometheusRuleGroup{ + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + Labels: map[string]string{ + "group_label": "group_value", + "common_label": "group_value", + }, + Rules: []PrometheusRule{ + { + Alert: "alert-1", + Expr: "cpu_usage > 80", + Labels: map[string]string{ + "rule_label": "rule_value", + "common_label": "rule_value", // rule-level label should take precedence + }, + }, + }, + } + + grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup) + require.NoError(t, err) + require.Len(t, grafanaGroup.Rules, 1) + + // Check that the labels are merged and the rule label takes precedence + require.Equal( + t, + map[string]string{ + "group_label": "group_value", + "rule_label": "rule_value", + "common_label": "rule_value", + }, + grafanaGroup.Rules[0].Labels, + ) + }) + + t.Run("group labels are merged with recording rule labels", func(t *testing.T) { + promGroup := PrometheusRuleGroup{ + Name: "recording-group", + Interval: prommodel.Duration(10 * time.Second), + Labels: map[string]string{ + "group_label": "group_value", + "common_label": "group_value", + }, + Rules: []PrometheusRule{ + { + Record: "recording_metric", + Expr: "sum(rate(http_requests_total[5m]))", + Labels: map[string]string{ + "rule_label": "rule_value", + "common_label": "rule_value", + }, + }, + }, + } + + grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup) + require.NoError(t, err) + require.Len(t, grafanaGroup.Rules, 1) + + // Check that the labels are merged and the rule label takes precedence + require.Equal( + t, + map[string]string{ + "group_label": "group_value", + "rule_label": "rule_value", + "common_label": "rule_value", + }, + grafanaGroup.Rules[0].Labels, + ) + }) + + t.Run("rule with no labels gets group labels", func(t *testing.T) { + promGroup := PrometheusRuleGroup{ + Name: "group-with-labels", + Interval: prommodel.Duration(10 * time.Second), + Labels: map[string]string{ + "group_label1": "group_value1", + "group_label2": "group_value2", + }, + Rules: []PrometheusRule{ + { + Alert: "alert-no-labels", + Expr: "up == 0", + }, + }, + } + + grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup) + require.NoError(t, err) + require.Len(t, grafanaGroup.Rules, 1) + + require.Equal(t, promGroup.Labels, grafanaGroup.Rules[0].Labels) + }) + + t.Run("rule and group with nil labels", func(t *testing.T) { + promGroup := PrometheusRuleGroup{ + Name: "group-no-labels", + Interval: prommodel.Duration(10 * time.Second), + Rules: []PrometheusRule{ + { + Alert: "alert-no-labels", + Expr: "up == 0", + }, + }, + } + + grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup) + require.NoError(t, err) + require.Len(t, grafanaGroup.Rules, 1) + require.Empty(t, grafanaGroup.Rules[0].Labels) + }) +} + func TestPrometheusRulesToGrafana_UID(t *testing.T) { orgID := int64(1) namespace := "some-namespace" diff --git a/pkg/services/ngalert/prom/models.go b/pkg/services/ngalert/prom/models.go index 6cde9b7badd..21ea45061dc 100644 --- a/pkg/services/ngalert/prom/models.go +++ b/pkg/services/ngalert/prom/models.go @@ -33,10 +33,6 @@ func (g *PrometheusRuleGroup) Validate() error { return ErrPrometheusRuleGroupValidationFailed.Errorf("limit is not supported") } - if len(g.Labels) > 0 { - return ErrPrometheusRuleGroupValidationFailed.Errorf("labels are not supported") - } - for _, rule := range g.Rules { if err := rule.Validate(); err != nil { return err diff --git a/pkg/services/ngalert/prom/models_test.go b/pkg/services/ngalert/prom/models_test.go index 00b6f0ac56b..b2bdb734b44 100644 --- a/pkg/services/ngalert/prom/models_test.go +++ b/pkg/services/ngalert/prom/models_test.go @@ -23,6 +23,9 @@ func TestPrometheusRuleGroup_Validate(t *testing.T) { group: PrometheusRuleGroup{ Name: "test_group", Interval: prommodel.Duration(60), + Labels: map[string]string{ + "label-1": "value-1", + }, Rules: []PrometheusRule{ { Alert: "test_alert", @@ -52,16 +55,6 @@ func TestPrometheusRuleGroup_Validate(t *testing.T) { expectError: true, errorMsg: "limit is not supported", }, - { - name: "invalid group with labels", - group: PrometheusRuleGroup{ - Name: "test_group", - Interval: prommodel.Duration(60), - Labels: map[string]string{"foo": "bar"}, - }, - expectError: true, - errorMsg: "labels are not supported", - }, { name: "invalid group with invalid rule", group: PrometheusRuleGroup{