diff --git a/pkg/services/ngalert/api/persist.go b/pkg/services/ngalert/api/persist.go index a3eb9af2b7c..52c4ca59a14 100644 --- a/pkg/services/ngalert/api/persist.go +++ b/pkg/services/ngalert/api/persist.go @@ -23,7 +23,7 @@ type RuleStore interface { GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) (*ngmodels.AlertRule, error) GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmodels.GetAlertRulesGroupByRuleUIDQuery) ([]*ngmodels.AlertRule, error) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) (ngmodels.RulesGroup, error) - ListAlertRulesByGroup(ctx context.Context, query *ngmodels.ListAlertRulesByGroupQuery) (ngmodels.RulesGroup, string, error) + ListAlertRulesByGroup(ctx context.Context, query *ngmodels.ListAlertRulesExtendedQuery) (ngmodels.RulesGroup, string, error) ListDeletedRules(ctx context.Context, orgID int64) ([]*ngmodels.AlertRule, error) // InsertAlertRules will insert all alert rules passed into the function diff --git a/pkg/services/ngalert/api/prometheus/api_prometheus.go b/pkg/services/ngalert/api/prometheus/api_prometheus.go index ae9ace0648a..b44f0f97651 100644 --- a/pkg/services/ngalert/api/prometheus/api_prometheus.go +++ b/pkg/services/ngalert/api/prometheus/api_prometheus.go @@ -245,7 +245,7 @@ type ListAlertRulesStore interface { } type ListAlertRulesStoreV2 interface { - ListAlertRulesByGroup(ctx context.Context, query *ngmodels.ListAlertRulesByGroupQuery) (ngmodels.RulesGroup, string, error) + ListAlertRulesByGroup(ctx context.Context, query *ngmodels.ListAlertRulesExtendedQuery) (ngmodels.RulesGroup, string, error) } func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) response.Response { @@ -487,7 +487,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt return ruleResponse } - byGroupQuery := ngmodels.ListAlertRulesByGroupQuery{ + byGroupQuery := ngmodels.ListAlertRulesExtendedQuery{ ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{ OrgID: opts.OrgID, NamespaceUIDs: namespaceUIDs, @@ -496,8 +496,8 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt RuleGroups: ruleGroups, ReceiverName: receiverName, }, - GroupLimit: maxGroups, - GroupContinueToken: nextToken, + Limit: maxGroups, + ContinueToken: nextToken, } ruleList, continueToken, err := store.ListAlertRulesByGroup(opts.Ctx, &byGroupQuery) if err != nil { diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 1b06daa84b5..9b9d211dd3b 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -938,14 +938,6 @@ const ( RuleTypeFilterRecording ) -type ListAlertRulesByGroupQuery struct { - ListAlertRulesQuery - RuleType RuleTypeFilter - - GroupLimit int64 // Number of groups to fetch - GroupContinueToken string // Token for per-group pagination -} - type GroupCursor struct { NamespaceUID string `json:"n"` RuleGroup string `json:"g"` diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 212c39143f8..044e0ce75bc 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -584,80 +584,17 @@ func (st DBstore) CountInFolders(ctx context.Context, orgID int64, folderUIDs [] return count, err } -func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.ListAlertRulesByGroupQuery) (result ngmodels.RulesGroup, nextToken string, err error) { +func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.ListAlertRulesExtendedQuery) (result ngmodels.RulesGroup, nextToken string, err error) { err = st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { - q := sess.Table("alert_rule") - - if query.OrgID >= 0 { - q = q.Where("org_id = ?", query.OrgID) + q, groupsSet, err := st.buildListAlertRulesQuery(sess, query) + if err != nil { + return err } - if query.DashboardUID != "" { - q = q.Where("dashboard_uid = ?", query.DashboardUID) - if query.PanelID != 0 { - q = q.Where("panel_id = ?", query.PanelID) - } - } - - if len(query.NamespaceUIDs) > 0 { - args, in := getINSubQueryArgs(query.NamespaceUIDs) - q = q.Where(fmt.Sprintf("namespace_uid IN (%s)", strings.Join(in, ",")), args...) - } - - if len(query.RuleUIDs) > 0 { - args, in := getINSubQueryArgs(query.RuleUIDs) - q = q.Where(fmt.Sprintf("uid IN (%s)", strings.Join(in, ",")), args...) - } - - var groupsMap map[string]struct{} - if len(query.RuleGroups) > 0 { - groupsMap = make(map[string]struct{}) - args, in := getINSubQueryArgs(query.RuleGroups) - q = q.Where(fmt.Sprintf("rule_group IN (%s)", strings.Join(in, ",")), args...) - for _, group := range query.RuleGroups { - groupsMap[group] = struct{}{} - } - } - - if query.ReceiverName != "" { - q, err = st.filterByContentInNotificationSettings(query.ReceiverName, q) - if err != nil { - return err - } - } - - if query.TimeIntervalName != "" { - q, err = st.filterByContentInNotificationSettings(query.TimeIntervalName, q) - if err != nil { - return err - } - } - - if query.HasPrometheusRuleDefinition != nil { - q, err = st.filterWithPrometheusRuleDefinition(*query.HasPrometheusRuleDefinition, q) - if err != nil { - return err - } - } - - switch query.RuleType { - case ngmodels.RuleTypeFilterAlerting: - q = q.Where("record = ''") - case ngmodels.RuleTypeFilterRecording: - q = q.Where("record != ''") - case ngmodels.RuleTypeFilterAll: - // no additional filter - default: - return fmt.Errorf("unknown rule type filter %q", query.RuleType) - } - - // Order by group first, then by rule index within group - q = q.Asc("namespace_uid", "rule_group", "rule_group_idx", "id") - var cursor ngmodels.GroupCursor - if query.GroupContinueToken != "" { + if query.ContinueToken != "" { // only set the cursor if it's valid, otherwise we'll start from the beginning - if cur, err := ngmodels.DecodeGroupCursor(query.GroupContinueToken); err == nil { + if cur, err := ngmodels.DecodeGroupCursor(query.ContinueToken); err == nil { cursor = cur } } @@ -701,7 +638,7 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis } if key != cursor { // Check if we've reached the group limit - if query.GroupLimit > 0 && groupsFetched == query.GroupLimit { + if query.Limit > 0 && groupsFetched == query.Limit { // Generate next token for the next group nextToken = ngmodels.EncodeGroupCursor(cursor) break @@ -713,7 +650,7 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis } // Apply post-query filters - if !shouldIncludeRule(&converted, query, groupsMap) { + if !shouldIncludeRule(&converted, query, groupsSet) { continue } @@ -731,7 +668,7 @@ func buildGroupCursorCondition(sess *xorm.Session, c ngmodels.GroupCursor) *xorm Or("(namespace_uid = ? AND rule_group > ?)", c.NamespaceUID, c.RuleGroup) } -func shouldIncludeRule(rule *ngmodels.AlertRule, query *ngmodels.ListAlertRulesByGroupQuery, groupsMap map[string]struct{}) bool { +func shouldIncludeRule(rule *ngmodels.AlertRule, query *ngmodels.ListAlertRulesExtendedQuery, groupsMap map[string]struct{}) bool { if query.ReceiverName != "" { if !slices.ContainsFunc(rule.NotificationSettings, func(settings ngmodels.NotificationSettings) bool { return settings.Receiver == query.ReceiverName @@ -786,6 +723,24 @@ func (st DBstore) ListAlertRulesPaginated(ctx context.Context, query *ngmodels.L if err != nil { return err } + + if query.ContinueToken != "" { + cursor, err := decodeCursor(query.ContinueToken) + if err != nil { + return fmt.Errorf("invalid continue token: %w", err) + } + + // Build cursor condition that matches the ORDER BY clause + q = buildCursorCondition(q, cursor) + } + + if query.Limit > 0 { + // Ensure we clamp to the max int available on the platform + lim := min(query.Limit, math.MaxInt) + // Fetch one extra rule to determine if there are more results + q = q.Limit(int(lim) + 1) + } + alertRules := make([]*ngmodels.AlertRule, 0) rule := new(alertRule) rows, err := q.Rows(rule) @@ -919,23 +874,6 @@ func (st DBstore) buildListAlertRulesQuery(sess *db.Session, query *ngmodels.Lis } q = q.Asc("namespace_uid", "rule_group", "rule_group_idx", "id") - - if query.ContinueToken != "" { - cursor, err := decodeCursor(query.ContinueToken) - if err != nil { - return nil, groupsSet, fmt.Errorf("invalid continue token: %w", err) - } - - // Build cursor condition that matches the ORDER BY clause - q = buildCursorCondition(q, cursor) - } - - if query.Limit > 0 { - // Ensure we clamp to the max int available on the platform - lim := min(query.Limit, math.MaxInt) - // Fetch one extra rule to determine if there are more results - q = q.Limit(int(lim) + 1) - } return q, groupsSet, nil } diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index ea6b339c6c6..90d557abf0d 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -1757,7 +1757,7 @@ func TestIntegration_ListAlertRulesByGroup(t *testing.T) { }) t.Run("should return all rules when no limit passed", func(t *testing.T) { - result, continueToken, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesByGroupQuery{ + result, continueToken, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesExtendedQuery{ ListAlertRulesQuery: models.ListAlertRulesQuery{OrgID: orgID}, }) require.NoError(t, err) @@ -1768,9 +1768,9 @@ func TestIntegration_ListAlertRulesByGroup(t *testing.T) { t.Run("should return paginated results when group limit is set", func(t *testing.T) { // random number from 1 to totalGroups - 1 (to ensure we always receive less than totalGroups) groupLimit := rand.Int64N(int64(totalGroups)-1) + 1 - result, continueToken, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesByGroupQuery{ + result, continueToken, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesExtendedQuery{ ListAlertRulesQuery: models.ListAlertRulesQuery{OrgID: orgID}, - GroupLimit: groupLimit, + Limit: groupLimit, }) require.NoError(t, err) expectedRuleCount := groupLimit * int64(rulesPerGroup) @@ -1780,9 +1780,9 @@ func TestIntegration_ListAlertRulesByGroup(t *testing.T) { t.Run("pagination should all for continuation", func(t *testing.T) { groupLimit := int64(2) // fixed group limit for this test - result, continueToken, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesByGroupQuery{ + result, continueToken, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesExtendedQuery{ ListAlertRulesQuery: models.ListAlertRulesQuery{OrgID: orgID}, - GroupLimit: groupLimit, + Limit: groupLimit, }) require.NoError(t, err) require.Len(t, result, int(groupLimit*int64(rulesPerGroup)), "should return rules for the first two groups") @@ -1798,9 +1798,9 @@ func TestIntegration_ListAlertRulesByGroup(t *testing.T) { resultRules = append(resultRules, result...) // Continue from previous, fetching the rest of the rules - result, continueToken, err = store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesByGroupQuery{ + result, continueToken, err = store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesExtendedQuery{ ListAlertRulesQuery: models.ListAlertRulesQuery{OrgID: orgID}, - GroupContinueToken: continueToken, + ContinueToken: continueToken, }) require.NoError(t, err) resultRules = append(resultRules, result...) @@ -1863,9 +1863,9 @@ func Benchmark_ListAlertRules(b *testing.B) { for _, groupLimit := range []int{1, 2, 5, 10, 50, 100} { b.Run(fmt.Sprintf("list %d groups paginated", groupLimit), func(b *testing.B) { for b.Loop() { - _, _, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesByGroupQuery{ + _, _, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesExtendedQuery{ ListAlertRulesQuery: models.ListAlertRulesQuery{OrgID: orgID}, - GroupLimit: int64(groupLimit), + Limit: int64(groupLimit), }) if err != nil { b.Fatal(err) diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 6b709bfd0e3..287a788399e 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -186,7 +186,7 @@ func (f *RuleStore) GetAlertRulesGroupByRuleUID(_ context.Context, q *models.Get return ruleList, nil } -func (f *RuleStore) ListAlertRulesByGroup(_ context.Context, q *models.ListAlertRulesByGroupQuery) (models.RulesGroup, string, error) { +func (f *RuleStore) ListAlertRulesByGroup(_ context.Context, q *models.ListAlertRulesExtendedQuery) (models.RulesGroup, string, error) { f.mtx.Lock() defer f.mtx.Unlock() f.RecordedOps = append(f.RecordedOps, *q) @@ -228,13 +228,13 @@ func (f *RuleStore) ListAlertRulesByGroup(_ context.Context, q *models.ListAlert var nextToken string var cursor models.GroupCursor - if q.GroupContinueToken != "" { - if cur, err := models.DecodeGroupCursor(q.GroupContinueToken); err == nil { + if q.ContinueToken != "" { + if cur, err := models.DecodeGroupCursor(q.ContinueToken); err == nil { cursor = cur } } - if q.GroupLimit < 0 { + if q.Limit < 0 { return ruleList, "", nil } @@ -254,7 +254,7 @@ func (f *RuleStore) ListAlertRulesByGroup(_ context.Context, q *models.ListAlert RuleGroup: r.RuleGroup, } if key != cursor { - if q.GroupLimit > 0 && groupsFetched == q.GroupLimit { + if q.Limit > 0 && groupsFetched == q.Limit { nextToken = models.EncodeGroupCursor(cursor) break }