Alerting: Evaluate all imported from Prometheus rules sequentially (#106295)

What is this feature?

Makes all alert rules imported from a Prometheus YAML or Prometheus-compatible data source evaluate sequentially.

Why do we need this feature?

Currently only alert rules [imported via the API](https://grafana.com/docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/alerting-migration/migration-api/) are evaluated sequentially, because only they have the original alert rule definition in YAML. But alert rules can be imported [in the UI, and from a YAML file](https://grafana.com/docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/alerting-migration/), and they won't be evaluated sequentially which can lead to issues with recording rules.
This commit is contained in:
Alexander Akhmetov
2025-06-05 12:08:44 +02:00
committed by GitHub
parent e4c9d10bfb
commit da88e5912f
13 changed files with 118 additions and 35 deletions
@@ -158,8 +158,8 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.
}
filterOpts := &provisioning.FilterOptions{
ImportedPrometheusRule: util.Pointer(true),
NamespaceUIDs: folderUIDs,
HasPrometheusRuleDefinition: util.Pointer(true),
NamespaceUIDs: folderUIDs,
}
groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
if err != nil {
@@ -193,8 +193,8 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contex
provenance := getProvenance(c)
filterOpts := &provisioning.FilterOptions{
NamespaceUIDs: []string{namespace.UID},
ImportedPrometheusRule: util.Pointer(true),
NamespaceUIDs: []string{namespace.UID},
HasPrometheusRuleDefinition: util.Pointer(true),
}
err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, provenance, filterOpts)
if errors.Is(err, models.ErrAlertRuleGroupNotFound) {
@@ -251,8 +251,8 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmo
}
filterOpts := &provisioning.FilterOptions{
ImportedPrometheusRule: util.Pointer(true),
NamespaceUIDs: []string{namespace.UID},
HasPrometheusRuleDefinition: util.Pointer(true),
NamespaceUIDs: []string{namespace.UID},
}
groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
if err != nil {
@@ -288,9 +288,9 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmo
}
filterOpts := &provisioning.FilterOptions{
ImportedPrometheusRule: util.Pointer(true),
NamespaceUIDs: []string{namespace.UID},
RuleGroups: []string{group},
HasPrometheusRuleDefinition: util.Pointer(true),
NamespaceUIDs: []string{namespace.UID},
RuleGroups: []string{group},
}
groupsWithFolders, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
if err != nil {
+7 -2
View File
@@ -406,7 +406,12 @@ func WithoutInternalLabels() LabelOption {
}
}
func (alertRule *AlertRule) ImportedFromPrometheus() bool {
func (alertRule *AlertRule) ImportedPrometheusRule() bool {
_, hasConvertedPrometheusRuleLabel := alertRule.GetLabels()[ConvertedPrometheusRuleLabel]
return hasConvertedPrometheusRuleLabel || alertRule.HasPrometheusRuleDefinition()
}
func (alertRule *AlertRule) HasPrometheusRuleDefinition() bool {
_, err := alertRule.PrometheusRuleDefinition()
return err == nil
}
@@ -859,7 +864,7 @@ type ListAlertRulesQuery struct {
ReceiverName string
TimeIntervalName string
ImportedPrometheusRule *bool
HasPrometheusRuleDefinition *bool
}
// CountAlertRulesQuery is the query for counting alert rules
+76 -1
View File
@@ -1259,7 +1259,7 @@ func TestAlertRule_PrometheusRuleDefinition(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tt.rule.PrometheusRuleDefinition()
isPrometheusRule := tt.rule.ImportedFromPrometheus()
isPrometheusRule := tt.rule.HasPrometheusRuleDefinition()
if tt.expectedErrorMsg != "" {
require.Error(t, err)
@@ -1273,3 +1273,78 @@ func TestAlertRule_PrometheusRuleDefinition(t *testing.T) {
})
}
}
func TestAlertRule_ImportedPrometheusRule(t *testing.T) {
tests := []struct {
name string
rule AlertRule
expected bool
}{
{
name: "rule with prometheus definition",
rule: AlertRule{
Metadata: AlertRuleMetadata{
PrometheusStyleRule: &PrometheusStyleRule{
OriginalRuleDefinition: "some rule definition",
},
},
},
expected: true,
},
{
name: "rule with converted prometheus rule label",
rule: AlertRule{
Labels: map[string]string{
ConvertedPrometheusRuleLabel: "true",
},
},
expected: true,
},
{
name: "rule with both prometheus definition and converted label",
rule: AlertRule{
Labels: map[string]string{
ConvertedPrometheusRuleLabel: "true",
},
Metadata: AlertRuleMetadata{
PrometheusStyleRule: &PrometheusStyleRule{
OriginalRuleDefinition: "some rule definition",
},
},
},
expected: true,
},
{
name: "rule with empty prometheus definition",
rule: AlertRule{
Metadata: AlertRuleMetadata{
PrometheusStyleRule: &PrometheusStyleRule{
OriginalRuleDefinition: "",
},
},
},
expected: false,
},
{
name: "rule with nil prometheus style rule",
rule: AlertRule{
Metadata: AlertRuleMetadata{
PrometheusStyleRule: nil,
},
},
expected: false,
},
{
name: "rule with empty metadata",
rule: AlertRule{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.rule.ImportedPrometheusRule()
require.Equal(t, tt.expected, result)
})
}
}
@@ -271,9 +271,9 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, user ident
// FilterOptions provides filtering for alert rule queries.
// All fields are optional and will be applied as filters if provided.
type FilterOptions struct {
ImportedPrometheusRule *bool
RuleGroups []string
NamespaceUIDs []string
HasPrometheusRuleDefinition *bool
RuleGroups []string
NamespaceUIDs []string
}
func (opts *FilterOptions) apply(q models.ListAlertRulesQuery) models.ListAlertRulesQuery {
@@ -281,8 +281,8 @@ func (opts *FilterOptions) apply(q models.ListAlertRulesQuery) models.ListAlertR
return q
}
if opts.ImportedPrometheusRule != nil {
q.ImportedPrometheusRule = opts.ImportedPrometheusRule
if opts.HasPrometheusRuleDefinition != nil {
q.HasPrometheusRuleDefinition = opts.HasPrometheusRuleDefinition
}
if len(opts.NamespaceUIDs) > 0 {
@@ -1924,8 +1924,8 @@ func TestDeleteRuleGroups(t *testing.T) {
t.Run("when filtering by imported Prometheus rules", func(t *testing.T) {
filterOpts := &FilterOptions{
ImportedPrometheusRule: util.Pointer(true),
NamespaceUIDs: []string{"namespace1"},
HasPrometheusRuleDefinition: util.Pointer(true),
NamespaceUIDs: []string{"namespace1"},
}
t.Run("when the group is not imported", func(t *testing.T) {
+1 -2
View File
@@ -88,8 +88,7 @@ func (sch *schedule) updateRulesMetrics(alertRules []*models.AlertRule) {
}
}
_, hasConvertedPrometheusRuleLabel := rule.GetLabels()[models.ConvertedPrometheusRuleLabel]
if rule.ImportedFromPrometheus() || hasConvertedPrometheusRuleLabel {
if rule.ImportedPrometheusRule() {
orgsRulesPrometheusImported[rule.OrgID]++
}
@@ -730,7 +730,7 @@ func TestSchedule_updateRulesMetrics(t *testing.T) {
})
// The metric includes alert rules with either internal ConvertedPrometheusRuleLabel label,
// or when AlertRule.ImportedFromPrometheus() returns true.
// or when AlertRule.HasPrometheusRuleDefinition() returns true.
alertRule1 := models.RuleGen.With(
models.RuleGen.WithOrgID(firstOrgID),
models.RuleGen.WithPrometheusOriginalRuleDefinition("1"),
+1 -1
View File
@@ -117,7 +117,7 @@ func (sch *schedule) shouldEvaluateSequentially(groupItems []readyToRunItem) boo
// only evaluate rules in imported groups sequentially
for _, item := range groupItems {
if item.rule.ImportedFromPrometheus() {
if item.rule.ImportedPrometheusRule() {
return true
}
}
@@ -90,6 +90,8 @@ func TestSequence(t *testing.T) {
models.RuleGen.WithUID("4"),
models.RuleGen.WithGroupIndex(2),
models.RuleGen.WithGroupName("rg2"),
// This rule has the Prometheus rule YAML definition,
// indicating it was converted from Prometheus.
models.RuleGen.WithPrometheusOriginalRuleDefinition("test"),
).GenerateRef(),
folderTitle: "folder1",
@@ -102,7 +104,9 @@ func TestSequence(t *testing.T) {
models.RuleGen.WithUID("5"),
models.RuleGen.WithGroupIndex(3),
models.RuleGen.WithGroupName("rg2"),
models.RuleGen.WithPrometheusOriginalRuleDefinition("test"),
// This rule does not have the YAML definition,
// but still has the label indicating it was converted from Prometheus.
models.RuleGen.WithLabel(models.ConvertedPrometheusRuleLabel, "true"),
).GenerateRef(),
folderTitle: "folder1",
},
+5 -5
View File
@@ -632,8 +632,8 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
}
}
if query.ImportedPrometheusRule != nil {
q, err = st.filterImportedPrometheusRules(*query.ImportedPrometheusRule, q)
if query.HasPrometheusRuleDefinition != nil {
q, err = st.filterWithPrometheusRuleDefinition(*query.HasPrometheusRuleDefinition, q)
if err != nil {
return err
}
@@ -678,8 +678,8 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
continue
}
}
if query.ImportedPrometheusRule != nil { // remove false-positive hits from the result
if *query.ImportedPrometheusRule != converted.ImportedFromPrometheus() {
if query.HasPrometheusRuleDefinition != nil { // remove false-positive hits from the result
if *query.HasPrometheusRuleDefinition != converted.HasPrometheusRuleDefinition() {
continue
}
}
@@ -1019,7 +1019,7 @@ func (st DBstore) filterByContentInNotificationSettings(value string, sess *xorm
return sess.And(sql, param), nil
}
func (st DBstore) filterImportedPrometheusRules(value bool, sess *xorm.Session) (*xorm.Session, error) {
func (st DBstore) filterWithPrometheusRuleDefinition(value bool, sess *xorm.Session) (*xorm.Session, error) {
if value {
// Filter for rules that have both prometheus_style_rule and original_rule_definition in metadata
return sess.And(
@@ -1743,7 +1743,7 @@ func TestIntegration_ListAlertRules(t *testing.T) {
ruleGen.WithIntervalMatching(cfg.UnifiedAlerting.BaseInterval),
ruleGen.WithOrgID(orgID),
)
t.Run("filter by ImportedPrometheusRule", func(t *testing.T) {
t.Run("filter by HasPrometheusRuleDefinition", func(t *testing.T) {
store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b)
regularRule := createRule(t, store, ruleGen)
importedRule := createRule(t, store, ruleGen.With(
@@ -1773,8 +1773,8 @@ func TestIntegration_ListAlertRules(t *testing.T) {
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
query := &models.ListAlertRulesQuery{
OrgID: orgID,
ImportedPrometheusRule: tt.importedPrometheusRule,
OrgID: orgID,
HasPrometheusRuleDefinition: tt.importedPrometheusRule,
}
result, err := store.ListAlertRules(context.Background(), query)
require.NoError(t, err)
+2 -2
View File
@@ -222,8 +222,8 @@ func (f *RuleStore) ListAlertRules(_ context.Context, q *models.ListAlertRulesQu
if len(q.RuleUIDs) > 0 && !slices.Contains(q.RuleUIDs, r.UID) {
continue
}
if q.ImportedPrometheusRule != nil {
if *q.ImportedPrometheusRule != r.ImportedFromPrometheus() {
if q.HasPrometheusRuleDefinition != nil {
if *q.HasPrometheusRuleDefinition != r.HasPrometheusRuleDefinition() {
continue
}
}