Alerting: Add Extended List Query for Alert Rules w/pagination (#109360)

* Alerting: Add Extended List Query for Alert Rules w/pagination

This adds an extended query which allows filtering by the kind of rule (Recording or Alerting) and supports pagination.

Pagination tokens will allow us to list all rules paginated, regardless of the rule group.

---------

Co-authored-by: William Wernert <william.wernert@grafana.com>
This commit is contained in:
Moustafa Baiou
2025-08-26 08:20:47 -04:00
committed by GitHub
co-authored by William Wernert
parent a7e4447d0e
commit 5724fae778
10 changed files with 474 additions and 41 deletions
@@ -939,6 +939,15 @@ type ListAlertRulesQuery struct {
HasPrometheusRuleDefinition *bool
}
type ListAlertRulesExtendedQuery struct {
ListAlertRulesQuery
RuleType RuleTypeFilter
Limit int64
ContinueToken string
}
// CountAlertRulesQuery is the query for counting alert rules
type CountAlertRulesQuery struct {
OrgID int64
@@ -14,6 +14,7 @@ type RuleAccessControlService interface {
AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error
AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error
AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error
HasAccessInFolder(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error)
}
func newRuleAccessControlService(ac RuleAccessControlService) *provisioningRuleAccessControl {
@@ -28,6 +28,7 @@ type ruleAccessControlService interface {
CanReadAllRules(ctx context.Context, user identity.Requester) (bool, error)
// CanWriteAllRules returns true if the user has full access to write rules via provisioning API and bypass regular checks
CanWriteAllRules(ctx context.Context, user identity.Requester) (bool, error)
HasAccessInFolder(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error)
}
var errProvenanceMismatch = errutil.NewBase(errutil.StatusConflict, "alerting.provenanceMismatch").MustTemplate(
@@ -80,6 +81,66 @@ func NewAlertRuleService(ruleStore RuleStore,
}
}
type ListAlertRulesOptions struct {
RuleType models.RuleTypeFilter
Limit int64
ContinueToken string
// TODO: plumb more options
}
func (service *AlertRuleService) ListAlertRules(ctx context.Context, user identity.Requester, opts ListAlertRulesOptions) (rules []*models.AlertRule, provenances map[string]models.Provenance, nextToken string, err error) {
q := models.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: models.ListAlertRulesQuery{
OrgID: user.GetOrgID(),
},
RuleType: opts.RuleType,
Limit: opts.Limit,
ContinueToken: opts.ContinueToken,
}
can, err := service.authz.CanReadAllRules(ctx, user)
if err != nil {
return nil, nil, "", err
}
// If user does not have blanket privilege to read rules, filter to only folders they have rule access to
if !can {
fq := folder.GetFoldersQuery{
OrgID: user.GetOrgID(),
SignedInUser: user,
}
folders, err := service.folderService.GetFolders(ctx, fq)
if err != nil {
return nil, nil, "", err
}
folderUIDs := make([]string, 0, len(folders))
for _, f := range folders {
access, err := service.authz.HasAccessInFolder(ctx, user, models.Namespace(*f.ToFolderReference()))
if err != nil {
return nil, nil, "", err
}
if access {
folderUIDs = append(folderUIDs, f.UID)
}
}
q.NamespaceUIDs = folderUIDs
}
rules, nextToken, err = service.ruleStore.ListAlertRulesPaginated(ctx, &q)
if err != nil {
return nil, nil, "", err
}
provenances = make(map[string]models.Provenance)
if len(rules) > 0 {
resourceType := rules[0].ResourceType()
provenances, err = service.provenanceStore.GetProvenances(ctx, user.GetOrgID(), resourceType)
if err != nil {
return nil, nil, "", err
}
}
return rules, provenances, nextToken, nil
}
func (service *AlertRuleService) GetAlertRules(ctx context.Context, user identity.Requester) ([]*models.AlertRule, map[string]models.Provenance, error) {
q := models.ListAlertRulesQuery{
OrgID: user.GetOrgID(),
@@ -1423,6 +1423,99 @@ func TestGetRuleGroup(t *testing.T) {
})
}
func TestListAlertRules(t *testing.T) {
orgID := rand.Int63()
u := &user.SignedInUser{OrgID: orgID}
groupKey1 := models.GenerateGroupKey(orgID)
groupKey2 := models.GenerateGroupKey(orgID)
gen := models.RuleGen
rules1 := gen.With(gen.WithGroupKey(groupKey1), gen.WithUniqueGroupIndex()).GenerateManyRef(3)
models.RulesGroup(rules1).SortByGroupIndex()
rules2 := gen.With(gen.WithGroupKey(groupKey2), gen.WithUniqueGroupIndex()).GenerateManyRef(4)
models.RulesGroup(rules2).SortByGroupIndex()
allRules := append(rules1, rules2...)
fs := foldertest.NewFakeService()
fs.AddFolder(&folder.Folder{
OrgID: orgID,
UID: groupKey1.NamespaceUID,
Title: "folder1",
})
fs.AddFolder(&folder.Folder{
OrgID: orgID,
UID: groupKey2.NamespaceUID,
Title: "folder2",
})
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
service, ruleStore, provenanceStore, ac := initService(t)
service.folderService = fs
ruleStore.Rules = map[int64][]*models.AlertRule{
orgID: allRules,
}
ac.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error) {
return true, nil
}
return service, ruleStore, provenanceStore, ac
}
t.Run("when user can read all rules", func(t *testing.T) {
t.Run("should skip AuthorizeRuleGroupRead and return all rules", func(t *testing.T) {
service, _, _, ac := initServiceWithData(t)
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
rules, _, token, err := service.ListAlertRules(context.Background(), u, ListAlertRulesOptions{})
require.NoError(t, err)
// check that rules contain all uids from allRules
ruleUIDs := make(map[string]bool)
for _, r := range rules {
ruleUIDs[r.UID] = true
}
for _, r := range allRules {
assert.True(t, ruleUIDs[r.UID])
}
require.Len(t, ruleUIDs, len(allRules))
require.Empty(t, token)
assert.Len(t, ac.Calls, 1)
assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
})
})
t.Run("when user cannot read all rules", func(t *testing.T) {
t.Run("should return only rules in accessible folders", func(t *testing.T) {
service, _, _, ac := initServiceWithData(t)
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return false, nil
}
ac.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error) {
return folder.GetNamespaceUID() == groupKey2.NamespaceUID, nil
}
rules, _, token, err := service.ListAlertRules(context.Background(), u, ListAlertRulesOptions{})
require.NoError(t, err)
// check that rules contain all uids from rules1
ruleUIDs := make(map[string]bool)
for _, r := range rules {
ruleUIDs[r.UID] = true
}
for _, r := range rules2 {
assert.True(t, ruleUIDs[r.UID])
}
require.Len(t, ruleUIDs, len(rules2))
require.Empty(t, token)
assert.Len(t, ac.Calls, 3)
assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
assert.Equal(t, "HasAccessInFolder", ac.Calls[1].Method)
assert.Equal(t, "HasAccessInFolder", ac.Calls[2].Method)
})
})
}
func TestGetAlertRules(t *testing.T) {
orgID := rand.Int63()
u := &user.SignedInUser{OrgID: orgID}
@@ -32,6 +32,7 @@ type TransactionManager interface {
type RuleStore interface {
GetAlertRuleByUID(ctx context.Context, query *models.GetAlertRuleByUIDQuery) (*models.AlertRule, error)
ListAlertRules(ctx context.Context, query *models.ListAlertRulesQuery) (models.RulesGroup, error)
ListAlertRulesPaginated(ctx context.Context, query *models.ListAlertRulesExtendedQuery) (models.RulesGroup, string, error)
GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error)
InsertAlertRules(ctx context.Context, user *models.UserUID, rule []models.AlertRule) ([]models.AlertRuleKeyWithId, error)
UpdateAlertRules(ctx context.Context, user *models.UserUID, rule []models.UpdateRule) error
@@ -113,6 +113,7 @@ type fakeRuleAccessControlService struct {
AuthorizeRuleChangesFunc func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error
CanReadAllRulesFunc func(ctx context.Context, user identity.Requester) (bool, error)
CanWriteAllRulesFunc func(ctx context.Context, user identity.Requester) (bool, error)
HasAccessInFolderFunc func(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error)
}
func (s *fakeRuleAccessControlService) RecordCall(method string, args ...interface{}) {
@@ -167,6 +168,14 @@ func (s *fakeRuleAccessControlService) CanWriteAllRules(ctx context.Context, use
return false, nil
}
func (s *fakeRuleAccessControlService) HasAccessInFolder(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error) {
s.RecordCall("HasAccessInFolder", ctx, user, folder)
if s.HasAccessInFolderFunc != nil {
return s.HasAccessInFolderFunc(ctx, user, folder)
}
return true, nil
}
type fakeAlertRuleNotificationStore struct {
Calls []call
+142 -39
View File
@@ -2,14 +2,16 @@ package store
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math"
"slices"
"strings"
"github.com/google/uuid"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"github.com/grafana/grafana/pkg/util/xorm"
@@ -762,8 +764,23 @@ func shouldIncludeRule(rule *ngmodels.AlertRule, query *ngmodels.ListAlertRulesB
return true
}
// ListAlertRules is a handler for retrieving alert rules of specific organisation.
func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) (result ngmodels.RulesGroup, err error) {
result, nextToken, err := st.ListAlertRulesPaginated(ctx, &ngmodels.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: *query,
ContinueToken: "",
Limit: 0,
RuleType: ngmodels.RuleTypeFilterAll,
})
// This should never happen, as Limit is 0, which means no pagination.
if nextToken != "" {
err = fmt.Errorf("unexpected next token %q, expected empty string", nextToken)
st.Logger.Error("ListAlertRules returned a next token, but it should not have, this is a bug!", "next_token", nextToken, "query", query)
}
return result, err
}
// ListAlertRulesPaginated is a handler for retrieving alert rules of specific organization paginated.
func (st DBstore) ListAlertRulesPaginated(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")
@@ -819,8 +836,37 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
}
}
// FIXME: record is nullable but we don't save it as null when it's nil
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)
}
q = q.Asc("namespace_uid", "rule_group", "rule_group_idx", "id")
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)
@@ -833,50 +879,107 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
// Deserialize each rule separately in case any of them contain invalid JSON.
for rows.Next() {
rule := new(alertRule)
err = rows.Scan(rule)
if err != nil {
st.Logger.Error("Invalid rule found in DB store, ignoring it", "func", "ListAlertRules", "error", err)
continue
converted, ok := st.handleRuleRow(rows, query, groupsMap)
if ok {
alertRules = append(alertRules, converted)
}
converted, err := alertRuleToModelsAlertRule(*rule, st.Logger)
if err != nil {
st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "ListAlertRules", "error", err)
continue
}
genToken := query.Limit > 0 && len(alertRules) > int(query.Limit)
if genToken {
// Remove the extra item we fetched
alertRules = alertRules[:query.Limit]
// Generate next continue token from the last item
lastRule := alertRules[len(alertRules)-1]
cursor := continueCursor{
NamespaceUID: lastRule.NamespaceUID,
RuleGroup: lastRule.RuleGroup,
RuleGroupIdx: int64(lastRule.RuleGroupIndex),
ID: lastRule.ID,
}
if query.ReceiverName != "" { // remove false-positive hits from the result
if !slices.ContainsFunc(converted.NotificationSettings, func(settings ngmodels.NotificationSettings) bool {
return settings.Receiver == query.ReceiverName
}) {
continue
}
}
if query.TimeIntervalName != "" {
if !slices.ContainsFunc(converted.NotificationSettings, func(settings ngmodels.NotificationSettings) bool {
return slices.Contains(settings.MuteTimeIntervals, query.TimeIntervalName) || slices.Contains(settings.ActiveTimeIntervals, query.TimeIntervalName)
}) {
continue
}
}
if query.HasPrometheusRuleDefinition != nil { // remove false-positive hits from the result
if *query.HasPrometheusRuleDefinition != converted.HasPrometheusRuleDefinition() {
continue
}
}
// MySQL (and potentially other databases) can use case-insensitive comparison.
// This code makes sure we return groups that only exactly match the filter.
if groupsMap != nil {
if _, ok := groupsMap[converted.RuleGroup]; !ok {
continue
}
}
alertRules = append(alertRules, &converted)
nextToken = encodeCursor(cursor)
}
result = alertRules
return nil
})
return result, err
return result, nextToken, err
}
func (st DBstore) handleRuleRow(rows *xorm.Rows, query *ngmodels.ListAlertRulesExtendedQuery, groupsSet map[string]struct{}) (*ngmodels.AlertRule, bool) {
rule := new(alertRule)
err := rows.Scan(rule)
if err != nil {
st.Logger.Error("Invalid rule found in DB store, ignoring it", "func", "ListAlertRules", "error", err)
return nil, false
}
converted, err := alertRuleToModelsAlertRule(*rule, st.Logger)
if err != nil {
st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "ListAlertRules", "error", err)
return nil, false
}
if query.ReceiverName != "" { // remove false-positive hits from the result
if !slices.ContainsFunc(converted.NotificationSettings, func(settings ngmodels.NotificationSettings) bool {
return settings.Receiver == query.ReceiverName
}) {
return nil, false
}
}
if query.TimeIntervalName != "" {
if !slices.ContainsFunc(converted.NotificationSettings, func(settings ngmodels.NotificationSettings) bool {
return slices.Contains(settings.MuteTimeIntervals, query.TimeIntervalName) || slices.Contains(settings.ActiveTimeIntervals, query.TimeIntervalName)
}) {
return nil, false
}
}
if query.HasPrometheusRuleDefinition != nil { // remove false-positive hits from the result
if *query.HasPrometheusRuleDefinition != converted.HasPrometheusRuleDefinition() {
return nil, false
}
}
// MySQL (and potentially other databases) can use case-insensitive comparison.
// This code makes sure we return groups that only exactly match the filter.
if groupsSet != nil {
if _, ok := groupsSet[converted.RuleGroup]; !ok {
return nil, false
}
}
return &converted, true
}
type continueCursor struct {
NamespaceUID string `json:"n"`
RuleGroup string `json:"g"`
RuleGroupIdx int64 `json:"i"`
ID int64 `json:"d"`
}
func encodeCursor(c continueCursor) string {
data, _ := json.Marshal(c)
return base64.URLEncoding.EncodeToString(data)
}
func decodeCursor(token string) (continueCursor, error) {
var c continueCursor
data, err := base64.URLEncoding.DecodeString(token)
if err != nil {
return c, fmt.Errorf("failed to decode token: %w", err)
}
if err := json.Unmarshal(data, &c); err != nil {
return c, fmt.Errorf("failed to unmarshal cursor: %w", err)
}
return c, nil
}
func buildCursorCondition(sess *xorm.Session, c continueCursor) *xorm.Session {
return sess.Where("(namespace_uid > ?)", c.NamespaceUID).
Or("(namespace_uid = ? AND rule_group > ?)", c.NamespaceUID, c.RuleGroup).
Or("(namespace_uid = ? AND rule_group = ? AND rule_group_idx > ?)", c.NamespaceUID, c.RuleGroup, c.RuleGroupIdx).
Or("(namespace_uid = ? AND rule_group = ? AND rule_group_idx = ? AND id > ?)", c.NamespaceUID, c.RuleGroup, c.RuleGroupIdx, c.ID)
}
// Count returns either the number of the alert rules under a specific org (if orgID is not zero)
@@ -1956,6 +1956,146 @@ func TestIntegration_ListAlertRules(t *testing.T) {
})
}
func TestIntegration_ListAlertRulesPaginated(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{
BaseInterval: time.Duration(rand.Int64N(100)) * time.Second,
}
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
b := &fakeBus{}
orgID := int64(1)
ruleGen := models.RuleGen
ruleGen = ruleGen.With(
ruleGen.WithIntervalMatching(cfg.UnifiedAlerting.BaseInterval),
ruleGen.WithOrgID(orgID),
)
t.Run("filter by RuleType", func(t *testing.T) {
store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b)
alertingGen := ruleGen
recordingGen := ruleGen.With(models.RuleMuts.WithAllRecordingRules(), models.RuleMuts.WithMetric("metric1"), models.RuleMuts.WithRecordFrom("A"))
alertingRules := []*models.AlertRule{
createRule(t, store, alertingGen),
createRule(t, store, alertingGen),
}
recordingRules := []*models.AlertRule{
createRule(t, store, recordingGen),
createRule(t, store, recordingGen),
}
t.Run("should return only alerting rules", func(t *testing.T) {
query := &models.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: models.ListAlertRulesQuery{
OrgID: orgID,
},
RuleType: models.RuleTypeFilterAlerting,
}
result, continueToken, err := store.ListAlertRulesPaginated(context.Background(), query)
require.NoError(t, err)
require.Empty(t, continueToken, "continue token should be empty when no pagination is applied")
require.NotEmpty(t, result)
for _, rule := range result {
require.Equal(t, models.RuleTypeAlerting, rule.Type())
}
})
t.Run("should return only recording rules", func(t *testing.T) {
query := &models.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: models.ListAlertRulesQuery{
OrgID: orgID,
},
RuleType: models.RuleTypeFilterRecording,
}
result, continueToken, err := store.ListAlertRulesPaginated(context.Background(), query)
require.NoError(t, err)
require.Empty(t, continueToken, "continue token should be empty when no pagination is applied")
require.NotEmpty(t, result)
for _, rule := range result {
require.Equal(t, models.RuleTypeRecording, rule.Type())
}
})
t.Run("should return both alerting and recording rules when RuleType is not set", func(t *testing.T) {
query := &models.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: models.ListAlertRulesQuery{
OrgID: orgID,
},
}
result, continueToken, err := store.ListAlertRulesPaginated(context.Background(), query)
require.NoError(t, err)
require.Empty(t, continueToken, "continue token should be empty when no pagination is applied")
require.NotEmpty(t, result)
var alertingCount, recordingCount int
for _, rule := range result {
switch rule.Type() {
case models.RuleTypeAlerting:
alertingCount++
case models.RuleTypeRecording:
recordingCount++
}
}
require.GreaterOrEqual(t, alertingCount, len(alertingRules))
require.GreaterOrEqual(t, recordingCount, len(recordingRules))
})
t.Run("should return both alerting and recording rules when RuleType is all", func(t *testing.T) {
query := &models.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: models.ListAlertRulesQuery{
OrgID: orgID,
},
RuleType: models.RuleTypeFilterAll,
}
result, continueToken, err := store.ListAlertRulesPaginated(context.Background(), query)
require.NoError(t, err)
require.Empty(t, continueToken, "continue token should be empty when no pagination is applied")
require.NotEmpty(t, result)
var alertingCount, recordingCount int
for _, rule := range result {
switch rule.Type() {
case models.RuleTypeAlerting:
alertingCount++
case models.RuleTypeRecording:
recordingCount++
}
}
require.GreaterOrEqual(t, alertingCount, len(alertingRules))
require.GreaterOrEqual(t, recordingCount, len(recordingRules))
})
})
t.Run("list rules with pagination", func(t *testing.T) {
store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b)
alertingGen := ruleGen.With(ruleGen.WithNamespaceUID("paginate-test"))
for i := 0; i < 10; i++ {
createRule(t, store, alertingGen)
}
t.Run("should return paginated results", func(t *testing.T) {
query := &models.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: models.ListAlertRulesQuery{
OrgID: orgID,
NamespaceUIDs: []string{"paginate-test"},
},
Limit: 5, // set page size to 5
}
result, continueToken, err := store.ListAlertRulesPaginated(context.Background(), query)
require.NoError(t, err)
require.Len(t, result, 5, "should return 5 rules as per page size")
require.NotEmpty(t, continueToken, "continue token should not be empty for paginated results")
// continue with the next page
query.ContinueToken = continueToken
result2, continueToken, err := store.ListAlertRulesPaginated(context.Background(), query)
require.NoError(t, err)
require.Len(t, result2, 5, "should return next 5 rules")
require.Empty(t, continueToken, "continue token should be empty when all rules are fetched")
require.NotElementsMatch(t, result, result2, "should not have same rules in both pages")
})
})
}
func TestIntegration_ListDeletedRules(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
+2 -2
View File
@@ -19,8 +19,8 @@ type alertRule struct {
DashboardUID *string `xorm:"dashboard_uid"`
PanelID *int64 `xorm:"panel_id"`
RuleGroup string
RuleGroupIndex int `xorm:"rule_group_idx"`
Record string
RuleGroupIndex int `xorm:"rule_group_idx"`
Record string // FIXME: record is nullable but we don't save it as null when it's nil
NoDataState string
ExecErrState string
For time.Duration
+16
View File
@@ -268,6 +268,22 @@ func (f *RuleStore) ListAlertRulesByGroup(_ context.Context, q *models.ListAlert
return outputRules, nextToken, nil
}
// TODO: implement pagination for this fake
func (f *RuleStore) ListAlertRulesPaginated(_ context.Context, q *models.ListAlertRulesExtendedQuery) (models.RulesGroup, string, error) {
f.mtx.Lock()
defer f.mtx.Unlock()
f.RecordedOps = append(f.RecordedOps, *q)
if err := f.Hook(*q); err != nil {
return nil, "", err
}
rules, err := f.listAlertRules(&q.ListAlertRulesQuery)
if err != nil {
return nil, "", err
}
return rules, "", nil
}
func (f *RuleStore) ListAlertRules(_ context.Context, q *models.ListAlertRulesQuery) (models.RulesGroup, error) {
f.mtx.Lock()
defer f.mtx.Unlock()