Alerting: Generate stable UIDs for alert rules in Prometheus conversion (#100973)

This commit is contained in:
Alexander Akhmetov 2025-02-22 11:06:42 +01:00 committed by GitHub
parent 436dc86a09
commit 5a6d9a99f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 142 additions and 0 deletions

View File

@ -5,10 +5,19 @@ import (
"fmt"
"time"
"github.com/google/uuid"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
)
const (
// ruleUIDLabel is a special label that can be used to set a custom UID for a Prometheus
// alert rule when converting it to a Grafana alert rule. If this label is not present,
// a stable UID will be generated automatically based on the rule's data.
ruleUIDLabel = "__grafana_alert_rule_uid__"
)
type Config struct {
@ -114,6 +123,12 @@ func (p *Converter) convertRuleGroup(orgID int64, namespaceUID string, promGroup
gr.Title = fmt.Sprintf("%s (%d)", gr.Title, val)
}
uid, err := getUID(orgID, namespaceUID, promGroup.Name, i, rule)
if err != nil {
return nil, fmt.Errorf("failed to generate UID for rule '%s': %w", gr.Title, err)
}
gr.UID = uid
rules = append(rules, gr)
}
@ -127,6 +142,24 @@ func (p *Converter) convertRuleGroup(orgID int64, namespaceUID string, promGroup
return result, nil
}
// getUID returns a UID for a Prometheus rule.
// If the rule has a special label its value is used.
// Otherwise, a stable UUID is generated by using a hash of the rule's data.
func getUID(orgID int64, namespaceUID string, group string, position int, promRule PrometheusRule) (string, error) {
if uid, ok := promRule.Labels[ruleUIDLabel]; ok {
if err := util.ValidateUID(uid); err != nil {
return "", fmt.Errorf("invalid UID label value: %s; %w", uid, err)
}
return uid, nil
}
// Generate stable UUID based on the orgID, namespace, group and position.
uidData := fmt.Sprintf("%d|%s|%s|%d", orgID, namespaceUID, group, position)
u := uuid.NewSHA1(uuid.NameSpaceOID, []byte(uidData))
return u.String(), nil
}
func (p *Converter) convertRule(orgID int64, namespaceUID, group string, rule PrometheusRule) (models.AlertRule, error) {
var forInterval time.Duration
if rule.For != nil {

View File

@ -1,15 +1,18 @@
package prom
import (
"fmt"
"testing"
"time"
"github.com/google/uuid"
prommodel "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
)
func TestPrometheusRulesToGrafana(t *testing.T) {
@ -136,6 +139,10 @@ func TestPrometheusRulesToGrafana(t *testing.T) {
expectedLabels[k] = v
}
uidData := fmt.Sprintf("%d|%s|%s|%d", tc.orgID, tc.namespace, tc.promGroup.Name, j)
u := uuid.NewSHA1(uuid.NameSpaceOID, []byte(uidData))
require.Equal(t, u.String(), grafanaRule.UID, tc.name)
require.Equal(t, expectedLabels, grafanaRule.Labels, tc.name)
require.Equal(t, promRule.Annotations, grafanaRule.Annotations, tc.name)
require.Equal(t, models.Duration(0*time.Minute), grafanaRule.Data[0].RelativeTimeRange.To)
@ -190,3 +197,105 @@ func TestPrometheusRulesToGrafanaWithDuplicateRuleNames(t *testing.T) {
require.Equal(t, "another alert", group.Rules[2].Title)
require.Equal(t, "alert (3)", group.Rules[3].Title)
}
func TestPrometheusRulesToGrafana_UID(t *testing.T) {
orgID := int64(1)
namespace := "some-namespace"
promGroup := PrometheusRuleGroup{
Name: "test-group-1",
Interval: prommodel.Duration(10 * time.Second),
Rules: []PrometheusRule{
{
Alert: "alert-1",
Expr: "cpu_usage > 80",
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
Labels: map[string]string{
"severity": "critical",
ruleUIDLabel: "rule-uid-1",
},
Annotations: map[string]string{
"summary": "CPU usage is critical",
},
},
},
}
converter, err := NewConverter(Config{
DatasourceUID: "datasource-uid",
DatasourceType: datasources.DS_PROMETHEUS,
})
require.NoError(t, err)
t.Run("if not specified, UID is generated based on the rule index", func(t *testing.T) {
grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
require.NoError(t, err)
firstUID := grafanaGroup.Rules[0].UID
// Convert again
grafanaGroup, err = converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
require.NoError(t, err)
secondUID := grafanaGroup.Rules[0].UID
// They must be equal
require.NotEmpty(t, firstUID)
require.Equal(t, firstUID, secondUID)
})
t.Run("if the special label is specified", func(t *testing.T) {
t.Run("and the label is valid it should be used", func(t *testing.T) {
orgID := int64(1)
namespace := "some-namespace"
converter, err := NewConverter(Config{
DatasourceUID: "datasource-uid",
DatasourceType: datasources.DS_PROMETHEUS,
})
require.NoError(t, err)
promGroup.Rules[0].Labels[ruleUIDLabel] = "rule-uid-1"
grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
require.NoError(t, err)
require.Equal(t, "rule-uid-1", grafanaGroup.Rules[0].UID)
})
t.Run("and the label is invalid", func(t *testing.T) {
orgID := int64(1)
namespace := "some-namespace"
converter, err := NewConverter(Config{
DatasourceUID: "datasource-uid",
DatasourceType: datasources.DS_PROMETHEUS,
})
require.NoError(t, err)
// create a string of 50 characters
promGroup.Rules[0].Labels[ruleUIDLabel] = "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllllmm" // too long
grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
require.Errorf(t, err, "invalid UID label value")
require.Nil(t, grafanaGroup)
})
t.Run("and the label is empty", func(t *testing.T) {
orgID := int64(1)
namespace := "some-namespace"
converter, err := NewConverter(Config{
DatasourceUID: "datasource-uid",
DatasourceType: datasources.DS_PROMETHEUS,
})
require.NoError(t, err)
promGroup.Rules[0].Labels[ruleUIDLabel] = ""
grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
require.Errorf(t, err, "invalid UID label value")
require.Nil(t, grafanaGroup)
})
})
}