Alerting: Add rule_limit parameter to the list rules API (#114055)
This commit is contained in:
@@ -1069,6 +1069,154 @@ func TestRouteGetRuleStatuses(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when requesting rules with rule_limit pagination", func(t *testing.T) {
|
||||
const namespaceUID = "namespace_0"
|
||||
|
||||
ruleStore := fakes.NewRuleStore(t)
|
||||
fakeAIM := NewFakeAlertInstanceManager(t)
|
||||
|
||||
// Generate 3 rule groups with 10 rules each
|
||||
allRules := make([]*ngmodels.AlertRule, 0, 30)
|
||||
for i := range 3 {
|
||||
rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
|
||||
RuleGroup: fmt.Sprintf("rule_group_%d", i),
|
||||
NamespaceUID: namespaceUID,
|
||||
OrgID: orgID,
|
||||
})).GenerateManyRef(10)
|
||||
|
||||
allRules = append(allRules, rules...)
|
||||
ruleStore.PutRule(context.Background(), rules...)
|
||||
}
|
||||
|
||||
api := NewPrometheusSrv(
|
||||
log.NewNopLogger(),
|
||||
fakeAIM,
|
||||
newFakeSchedulerReader(t).setupStates(fakeAIM),
|
||||
ruleStore,
|
||||
accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures())),
|
||||
fakes.NewFakeProvisioningStore(),
|
||||
)
|
||||
|
||||
permissions := createPermissionsForRules(allRules, orgID)
|
||||
user := &user.SignedInUser{
|
||||
OrgID: orgID,
|
||||
Permissions: permissions,
|
||||
}
|
||||
c := &contextmodel.ReqContext{
|
||||
SignedInUser: user,
|
||||
}
|
||||
|
||||
t.Run("should return complete groups until rule_limit is met", func(t *testing.T) {
|
||||
// With rule_limit=15, should return group-0 (10) + group-1 (10)
|
||||
// Even though 20 > 15, we never return partial groups
|
||||
r, err := http.NewRequest("GET", "/api/v1/rules?rule_limit=15", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
c.Context = &web.Context{Req: r}
|
||||
|
||||
resp := api.RouteGetRuleStatuses(c)
|
||||
require.Equal(t, http.StatusOK, resp.Status())
|
||||
result := &apimodels.RuleResponse{}
|
||||
require.NoError(t, json.Unmarshal(resp.Body(), result))
|
||||
|
||||
require.Len(t, result.Data.RuleGroups, 2, "should return 2 groups")
|
||||
require.Len(t, result.Data.Totals, 0)
|
||||
require.Equal(t, "rule_group_0", result.Data.RuleGroups[0].Name)
|
||||
require.Equal(t, "rule_group_1", result.Data.RuleGroups[1].Name)
|
||||
require.Len(t, result.Data.RuleGroups[0].Rules, 10)
|
||||
require.Len(t, result.Data.RuleGroups[1].Rules, 10)
|
||||
|
||||
expectedToken := ngmodels.EncodeGroupCursor(ngmodels.GroupCursor{
|
||||
NamespaceUID: namespaceUID,
|
||||
RuleGroup: "rule_group_1",
|
||||
})
|
||||
require.Equal(t, expectedToken, result.Data.NextToken)
|
||||
})
|
||||
|
||||
t.Run("should return two groups when the number of alerts == limit", func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "/api/v1/rules?rule_limit=20", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
c.Context = &web.Context{Req: r}
|
||||
|
||||
resp := api.RouteGetRuleStatuses(c)
|
||||
require.Equal(t, http.StatusOK, resp.Status())
|
||||
result := &apimodels.RuleResponse{}
|
||||
require.NoError(t, json.Unmarshal(resp.Body(), result))
|
||||
|
||||
require.Len(t, result.Data.RuleGroups, 2, "should return 2 groups")
|
||||
require.Len(t, result.Data.Totals, 0)
|
||||
require.Equal(t, "rule_group_0", result.Data.RuleGroups[0].Name)
|
||||
require.Equal(t, "rule_group_1", result.Data.RuleGroups[1].Name)
|
||||
require.Len(t, result.Data.RuleGroups[0].Rules, 10)
|
||||
require.Len(t, result.Data.RuleGroups[1].Rules, 10)
|
||||
|
||||
expectedToken := ngmodels.EncodeGroupCursor(ngmodels.GroupCursor{
|
||||
NamespaceUID: namespaceUID,
|
||||
RuleGroup: "rule_group_1",
|
||||
})
|
||||
require.Equal(t, expectedToken, result.Data.NextToken)
|
||||
})
|
||||
|
||||
t.Run("should respect group_limit when it is reached first", func(t *testing.T) {
|
||||
// group_limit=1 with rule_limit=100: group limit reached first
|
||||
r, err := http.NewRequest("GET", "/api/v1/rules?group_limit=1&rule_limit=100", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
c.Context = &web.Context{Req: r}
|
||||
|
||||
resp := api.RouteGetRuleStatuses(c)
|
||||
require.Equal(t, http.StatusOK, resp.Status())
|
||||
result := &apimodels.RuleResponse{}
|
||||
require.NoError(t, json.Unmarshal(resp.Body(), result))
|
||||
|
||||
require.Len(t, result.Data.RuleGroups, 1, "should return 1 group (group_limit=1)")
|
||||
require.Len(t, result.Data.Totals, 0)
|
||||
|
||||
expectedToken := ngmodels.EncodeGroupCursor(ngmodels.GroupCursor{
|
||||
NamespaceUID: namespaceUID,
|
||||
RuleGroup: "rule_group_0",
|
||||
})
|
||||
require.Equal(t, expectedToken, result.Data.NextToken)
|
||||
})
|
||||
|
||||
t.Run("should respect rule_limit when it is reached first", func(t *testing.T) {
|
||||
// rule_limit=15 with group_limit=5: rule limit reached first (returns 2 groups, 20 rules)
|
||||
r, err := http.NewRequest("GET", "/api/v1/rules?group_limit=5&rule_limit=15", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
c.Context = &web.Context{Req: r}
|
||||
|
||||
resp := api.RouteGetRuleStatuses(c)
|
||||
require.Equal(t, http.StatusOK, resp.Status())
|
||||
result := &apimodels.RuleResponse{}
|
||||
require.NoError(t, json.Unmarshal(resp.Body(), result))
|
||||
|
||||
require.Len(t, result.Data.RuleGroups, 2, "should return 2 groups (20 rules exceeds rule_limit=15)")
|
||||
require.Len(t, result.Data.Totals, 0)
|
||||
|
||||
expectedToken := ngmodels.EncodeGroupCursor(ngmodels.GroupCursor{
|
||||
NamespaceUID: namespaceUID,
|
||||
RuleGroup: "rule_group_1",
|
||||
})
|
||||
require.Equal(t, expectedToken, result.Data.NextToken)
|
||||
})
|
||||
|
||||
t.Run("should return nothing when using rule_limit=0", func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "/api/v1/rules?rule_limit=0", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
c.Context = &web.Context{Req: r}
|
||||
|
||||
resp := api.RouteGetRuleStatuses(c)
|
||||
require.Equal(t, http.StatusOK, resp.Status())
|
||||
result := &apimodels.RuleResponse{}
|
||||
require.NoError(t, json.Unmarshal(resp.Body(), result))
|
||||
|
||||
require.Len(t, result.Data.RuleGroups, 0)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when fine-grained access is enabled", func(t *testing.T) {
|
||||
t.Run("should return only rules if the user can query all data sources", func(t *testing.T) {
|
||||
ruleStore := fakes.NewRuleStore(t)
|
||||
|
||||
@@ -492,10 +492,22 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
ruleType = ngmodels.RuleTypeFilterAll
|
||||
}
|
||||
|
||||
// Pagination limits
|
||||
//
|
||||
// group_limit: Maximum number of rule groups to return
|
||||
// - Returns exactly this many groups or fewer if not enough exist
|
||||
//
|
||||
// rule_limit: Maximum number of rules to return across all groups
|
||||
// - Returns complete groups until total rules meets or exceeds this limit
|
||||
// - May exceed the rule limit if needed to include the final complete group
|
||||
// - Example: rule_limit=15 with groups of [10, 10, 10] rules returns first 2 groups, 20 rules total
|
||||
//
|
||||
// When both limits are specified, whichever limit is reached first takes precedence.
|
||||
maxGroups := getInt64WithDefault(opts.Query, "group_limit", -1)
|
||||
maxRules := getInt64WithDefault(opts.Query, "rule_limit", -1)
|
||||
nextToken := opts.Query.Get("group_next_token")
|
||||
|
||||
if maxGroups == 0 {
|
||||
if maxGroups == 0 || maxRules == 0 {
|
||||
return ruleResponse
|
||||
}
|
||||
|
||||
@@ -512,6 +524,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
},
|
||||
RuleType: ruleType,
|
||||
Limit: maxGroups,
|
||||
RuleLimit: maxRules,
|
||||
ContinueToken: nextToken,
|
||||
}
|
||||
ruleList, continueToken, err := store.ListAlertRulesByGroup(opts.Ctx, &byGroupQuery)
|
||||
@@ -557,7 +570,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
ruleResponse.Data.NextToken = continueToken
|
||||
|
||||
// Only return Totals if there is no pagination
|
||||
if maxGroups == -1 {
|
||||
if maxGroups == -1 && maxRules == -1 {
|
||||
ruleResponse.Data.Totals = rulesTotals
|
||||
}
|
||||
|
||||
|
||||
@@ -994,6 +994,7 @@ type ListAlertRulesExtendedQuery struct {
|
||||
RuleType RuleTypeFilter
|
||||
|
||||
Limit int64
|
||||
RuleLimit int64
|
||||
ContinueToken string
|
||||
}
|
||||
|
||||
|
||||
@@ -618,6 +618,7 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis
|
||||
|
||||
// Process rules and implement per-group pagination
|
||||
var groupsFetched int64
|
||||
var rulesFetched int64
|
||||
for rows.Next() {
|
||||
rule := new(alertRule)
|
||||
err = rows.Scan(rule)
|
||||
@@ -644,6 +645,11 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis
|
||||
nextToken = ngmodels.EncodeGroupCursor(cursor)
|
||||
break
|
||||
}
|
||||
// Check if we've reached the rule limit
|
||||
if query.RuleLimit > 0 && rulesFetched >= query.RuleLimit {
|
||||
nextToken = ngmodels.EncodeGroupCursor(cursor)
|
||||
break
|
||||
}
|
||||
|
||||
// Reset for new group
|
||||
cursor = key
|
||||
@@ -656,6 +662,7 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis
|
||||
}
|
||||
|
||||
alertRules = append(alertRules, &converted)
|
||||
rulesFetched++
|
||||
}
|
||||
|
||||
result = alertRules
|
||||
|
||||
@@ -234,12 +234,13 @@ func (f *RuleStore) ListAlertRulesByGroup(_ context.Context, q *models.ListAlert
|
||||
}
|
||||
}
|
||||
|
||||
if q.Limit < 0 {
|
||||
if q.Limit < 0 && q.RuleLimit < 0 {
|
||||
return ruleList, "", nil
|
||||
}
|
||||
|
||||
outputRules := make([]*models.AlertRule, 0, len(ruleList))
|
||||
var groupsFetched int64
|
||||
var rulesFetched int64
|
||||
initialCursor := cursor
|
||||
for _, r := range ruleList {
|
||||
// skip rules before the initial cursor
|
||||
@@ -258,11 +259,16 @@ func (f *RuleStore) ListAlertRulesByGroup(_ context.Context, q *models.ListAlert
|
||||
nextToken = models.EncodeGroupCursor(cursor)
|
||||
break
|
||||
}
|
||||
if q.RuleLimit > 0 && rulesFetched >= q.RuleLimit {
|
||||
nextToken = models.EncodeGroupCursor(cursor)
|
||||
break
|
||||
}
|
||||
cursor = key
|
||||
groupsFetched++
|
||||
}
|
||||
|
||||
outputRules = append(outputRules, r)
|
||||
rulesFetched++
|
||||
}
|
||||
|
||||
return outputRules, nextToken, nil
|
||||
|
||||
Reference in New Issue
Block a user