Alerting: Add filters for RouteGetRuleStatuses (#88295)

* Placeholder commit with rule_uid change

* Add new filters to grafana rule state API

* Revert type change

* Split rule_group and rule_name params

* remove debug line

* Change how query params are parsed

* Comment
This commit is contained in:
Fayzal Ghantiwala 2024-06-04 10:57:55 +01:00 committed by GitHub
parent 783aa1c0b4
commit b66cd7ef79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 164 additions and 4 deletions

View File

@ -235,6 +235,9 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
}
// TODO: Refactor this function to reduce the cylomatic complexity
//
//nolint:gocyclo
func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse {
ruleResponse := apimodels.RuleResponse{
DiscoveryBase: apimodels.DiscoveryBase{
@ -292,16 +295,26 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager
return ruleResponse
}
namespaceUIDs := make([]string, len(opts.Namespaces))
for k := range opts.Namespaces {
namespaceUIDs = append(namespaceUIDs, k)
namespaceUIDs := make([]string, 0, len(opts.Namespaces))
folderUID := opts.Query.Get("folder_uid")
_, exists := opts.Namespaces[folderUID]
if folderUID != "" && exists {
namespaceUIDs = append(namespaceUIDs, folderUID)
} else {
for k := range opts.Namespaces {
namespaceUIDs = append(namespaceUIDs, k)
}
}
ruleGroups := opts.Query["rule_group"]
alertRuleQuery := ngmodels.ListAlertRulesQuery{
OrgID: opts.OrgID,
NamespaceUIDs: namespaceUIDs,
DashboardUID: dashboardUID,
PanelID: panelID,
RuleGroups: ruleGroups,
}
ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery)
if err != nil {
@ -311,10 +324,23 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager
return ruleResponse
}
ruleNames := opts.Query["rule_name"]
ruleNamesSet := make(map[string]struct{}, len(ruleNames))
for _, rn := range ruleNames {
ruleNamesSet[rn] = struct{}{}
}
// Group rules together by Namespace and Rule Group. Rules are also grouped by Org ID,
// but in this API all rules belong to the same organization.
// but in this API all rules belong to the same organization. Also filter by rule name if
// it was provided as a query param.
groupedRules := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
for _, rule := range ruleList {
if len(ruleNamesSet) > 0 {
if _, exists := ruleNamesSet[rule.Title]; !exists {
continue
}
}
groupKey := rule.GetGroupKey()
ruleGroup := groupedRules[groupKey]
ruleGroup = append(ruleGroup, rule)
@ -343,6 +369,7 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager
if !ok {
continue
}
ruleGroup, totals := toRuleGroup(log, manager, groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions)
ruleGroup.Totals = totals
for k, v := range totals {

View File

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"testing"
"time"
@ -527,6 +528,138 @@ func TestRouteGetRuleStatuses(t *testing.T) {
})
})
t.Run("test folder, group and rule name query params", func(t *testing.T) {
ruleStore := fakes.NewRuleStore(t)
fakeAIM := NewFakeAlertInstanceManager(t)
rulesInGroup1 := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
RuleGroup: "rule-group-1",
NamespaceUID: "folder-1",
OrgID: orgID,
})).GenerateManyRef(1)
rulesInGroup2 := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
RuleGroup: "rule-group-2",
NamespaceUID: "folder-2",
OrgID: orgID,
})).GenerateManyRef(2)
rulesInGroup3 := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
RuleGroup: "rule-group-3",
NamespaceUID: "folder-1",
OrgID: orgID,
})).GenerateManyRef(3)
ruleStore.PutRule(context.Background(), rulesInGroup1...)
ruleStore.PutRule(context.Background(), rulesInGroup2...)
ruleStore.PutRule(context.Background(), rulesInGroup3...)
api := PrometheusSrv{
log: log.NewNopLogger(),
manager: fakeAIM,
store: ruleStore,
authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures())),
}
permissions := createPermissionsForRules(slices.Concat(rulesInGroup1, rulesInGroup2, rulesInGroup3), orgID)
user := &user.SignedInUser{
OrgID: orgID,
Permissions: permissions,
}
c := &contextmodel.ReqContext{
SignedInUser: user,
}
t.Run("should only return rule groups under given folder_uid", func(t *testing.T) {
r, err := http.NewRequest("GET", "/api/v1/rules?folder_uid=folder-1", 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)
require.Equal(t, "rule-group-1", result.Data.RuleGroups[0].Name)
require.Equal(t, "rule-group-3", result.Data.RuleGroups[1].Name)
})
t.Run("should only return rule groups under given rule_group list", func(t *testing.T) {
r, err := http.NewRequest("GET", "/api/v1/rules?rule_group=rule-group-1&rule_group=rule-group-2", 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)
require.True(t, true, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool {
return rg.Name == "rule-group-1"
}))
require.True(t, true, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool {
return rg.Name == "rule-group-2"
}))
})
t.Run("should only return rule under given rule_name list", func(t *testing.T) {
expectedRuleInGroup2 := rulesInGroup2[0]
expectedRuleInGroup3 := rulesInGroup3[0]
r, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?rule_name=%s&rule_name=%s", expectedRuleInGroup2.Title, expectedRuleInGroup3.Title), 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)
require.True(t, true, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool {
return rg.Name == "rule-group-2"
}))
require.True(t, true, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool {
return rg.Name == "rule-group-3"
}))
require.Len(t, result.Data.RuleGroups[0].Rules, 1)
require.Len(t, result.Data.RuleGroups[1].Rules, 1)
if result.Data.RuleGroups[0].Name == "rule-group-2" {
require.Equal(t, expectedRuleInGroup2.Title, result.Data.RuleGroups[0].Rules[0].Name)
require.Equal(t, expectedRuleInGroup3.Title, result.Data.RuleGroups[1].Rules[0].Name)
} else {
require.Equal(t, expectedRuleInGroup2.Title, result.Data.RuleGroups[1].Rules[0].Name)
require.Equal(t, expectedRuleInGroup3.Title, result.Data.RuleGroups[0].Rules[0].Name)
}
})
t.Run("should only return rule with given folder_uid, rule_group and rule_name", func(t *testing.T) {
expectedRule := rulesInGroup3[2]
r, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?folder_uid=folder-1&rule_group=rule-group-3&rule_name=%s", expectedRule.Title), 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)
folder, err := api.store.GetNamespaceByUID(context.Background(), "folder-1", orgID, user)
require.NoError(t, err)
require.Equal(t, folder.Fullpath, result.Data.RuleGroups[0].File)
require.Equal(t, "rule-group-3", result.Data.RuleGroups[0].Name)
require.Len(t, result.Data.RuleGroups[0].Rules, 1)
require.Equal(t, expectedRule.Title, result.Data.RuleGroups[0].Rules[0].Name)
})
})
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)