Alerting: Update prometheus api to reuse list query logic

This lets the prometheus api respect NoGroup query logic and treat non-grouped rules consistently.

Co-authored-by: William Wernert <william.wernert@grafana.com>
This commit is contained in:
Moustafa Baiou 2025-08-28 16:50:56 -04:00 committed by Moustafa Baiou
parent ca8324e62a
commit f65e219b21
6 changed files with 46 additions and 116 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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"`

View File

@ -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
}

View File

@ -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)

View File

@ -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
}