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
@@ -58,7 +58,7 @@ Plugin rules that have the label `__grafana_origin` are not included on alert ru
### Evaluation of imported rules
The imported rules are evaluated sequentially within each rule group, mirroring Prometheus behavior. Sequential evaluation applies to rules only while they remain readonly (displayed as "Provisioned"). If you import rules with the `X-Disable-Provenance: true` header or via the regular provisioning API, they behave like regular Grafana alert rules and are evaluated in parallel.
The imported rules are evaluated sequentially within each rule group, mirroring Prometheus behavior. This behavior is different from native Grafana-managed alert rules, where the evaluation order is not enforced.
## Import alert rules
@@ -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
}
}