Alerting: Support group-level labels in Prometheus to Grafana conversion (#101624)
This commit is contained in:
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user