Alerting: Support group-level labels in Prometheus to Grafana conversion (#101624)

pull/101441/head^2
Alexander Akhmetov 4 months ago committed by GitHub
parent b8356d2234
commit a93a05aebd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 16
      pkg/services/ngalert/prom/convert.go
  2. 136
      pkg/services/ngalert/prom/convert_test.go
  3. 4
      pkg/services/ngalert/prom/models.go
  4. 13
      pkg/services/ngalert/prom/models_test.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{

@ -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"

@ -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

@ -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{

Loading…
Cancel
Save