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:
committed by
GitHub
parent
e4c9d10bfb
commit
da88e5912f
@@ -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 read‑only (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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user