3017 lines
103 KiB
Go
3017 lines
103 KiB
Go
package api
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
"slices"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
|
||
alertingModels "github.com/grafana/alerting/models"
|
||
|
||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||
|
||
"github.com/grafana/grafana/pkg/expr"
|
||
"github.com/grafana/grafana/pkg/infra/log"
|
||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||
"github.com/grafana/grafana/pkg/services/datasources"
|
||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||
"github.com/grafana/grafana/pkg/services/folder"
|
||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||
"github.com/grafana/grafana/pkg/services/ngalert/state"
|
||
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||
"github.com/grafana/grafana/pkg/services/user"
|
||
"github.com/grafana/grafana/pkg/util"
|
||
"github.com/grafana/grafana/pkg/web"
|
||
|
||
. "github.com/grafana/grafana/pkg/services/ngalert/api/prometheus"
|
||
)
|
||
|
||
func Test_FormatValues(t *testing.T) {
|
||
val1 := 1.1
|
||
val2 := 1.4
|
||
|
||
tc := []struct {
|
||
name string
|
||
alertState *state.State
|
||
expected string
|
||
}{
|
||
{
|
||
name: "with no value, it renders the evaluation string",
|
||
alertState: &state.State{
|
||
LastEvaluationString: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
||
LatestResult: &state.Evaluation{Condition: "A", Values: map[string]float64{}},
|
||
},
|
||
expected: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
||
},
|
||
{
|
||
name: "with one value, it renders the single value",
|
||
alertState: &state.State{
|
||
LastEvaluationString: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
||
LatestResult: &state.Evaluation{Condition: "A", Values: map[string]float64{"A": val1}},
|
||
},
|
||
expected: "1.1e+00",
|
||
},
|
||
{
|
||
name: "with two values, it renders the value based on their refID and position",
|
||
alertState: &state.State{
|
||
LastEvaluationString: "[ var='B0' metric='vector(10) + time() % 50' labels={} value=1.1 ], [ var='B1' metric='vector(10) + time() % 50' labels={} value=1.4 ]",
|
||
LatestResult: &state.Evaluation{Condition: "B", Values: map[string]float64{"B0": val1, "B1": val2}},
|
||
},
|
||
expected: "B0: 1.1e+00, B1: 1.4e+00",
|
||
},
|
||
{
|
||
name: "with a high number of values, it renders the value based on their refID and position using a natural order",
|
||
alertState: &state.State{
|
||
LastEvaluationString: "[ var='B0' metric='vector(10) + time() % 50' labels={} value=1.1 ], [ var='B1' metric='vector(10) + time() % 50' labels={} value=1.4 ]",
|
||
LatestResult: &state.Evaluation{Condition: "B", Values: map[string]float64{"B0": val1, "B1": val2, "B2": val1, "B10": val2, "B11": val1}},
|
||
},
|
||
expected: "B0: 1.1e+00, B10: 1.4e+00, B11: 1.1e+00, B1: 1.4e+00, B2: 1.1e+00",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tc {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
require.Equal(t, tt.expected, FormatValues(tt.alertState))
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestRouteGetAlertStatuses(t *testing.T) {
|
||
orgID := int64(1)
|
||
|
||
t.Run("with no alerts", func(t *testing.T) {
|
||
_, _, api := setupAPI(t)
|
||
req, err := http.NewRequest("GET", "/api/v1/alerts", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}}
|
||
|
||
r := api.RouteGetAlertStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
require.JSONEq(t, `
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"alerts": []
|
||
}
|
||
}
|
||
`, string(r.Body()))
|
||
})
|
||
|
||
t.Run("with two alerts", func(t *testing.T) {
|
||
_, fakeAIM, api := setupAPI(t)
|
||
fakeAIM.GenerateAlertInstances(1, util.GenerateShortUID(), 2)
|
||
req, err := http.NewRequest("GET", "/api/v1/alerts", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}}
|
||
|
||
r := api.RouteGetAlertStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
require.JSONEq(t, `
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"alerts": [{
|
||
"labels": {
|
||
"alertname": "test_title_0",
|
||
"instance_label": "test",
|
||
"label": "test"
|
||
},
|
||
"annotations": {
|
||
"annotation": "test"
|
||
},
|
||
"state": "Normal",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": ""
|
||
}, {
|
||
"labels": {
|
||
"alertname": "test_title_1",
|
||
"instance_label": "test",
|
||
"label": "test"
|
||
},
|
||
"annotations": {
|
||
"annotation": "test"
|
||
},
|
||
"state": "Normal",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": ""
|
||
}]
|
||
}
|
||
}`, string(r.Body()))
|
||
})
|
||
|
||
t.Run("with two firing alerts", func(t *testing.T) {
|
||
_, fakeAIM, api := setupAPI(t)
|
||
fakeAIM.GenerateAlertInstances(1, util.GenerateShortUID(), 2, withAlertingState())
|
||
req, err := http.NewRequest("GET", "/api/v1/alerts", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}}
|
||
|
||
r := api.RouteGetAlertStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
require.JSONEq(t, `
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"alerts": [{
|
||
"labels": {
|
||
"alertname": "test_title_0",
|
||
"instance_label": "test",
|
||
"label": "test"
|
||
},
|
||
"annotations": {
|
||
"annotation": "test"
|
||
},
|
||
"state": "Alerting",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": "1.1e+00"
|
||
}, {
|
||
"labels": {
|
||
"alertname": "test_title_1",
|
||
"instance_label": "test",
|
||
"label": "test"
|
||
},
|
||
"annotations": {
|
||
"annotation": "test"
|
||
},
|
||
"state": "Alerting",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": "1.1e+00"
|
||
}]
|
||
}
|
||
}`, string(r.Body()))
|
||
})
|
||
|
||
t.Run("with a recovering alert", func(t *testing.T) {
|
||
_, fakeAIM, api := setupAPI(t)
|
||
fakeAIM.GenerateAlertInstances(1, util.GenerateShortUID(), 1, withRecoveringState())
|
||
req, err := http.NewRequest("GET", "/api/v1/alerts", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}}
|
||
|
||
r := api.RouteGetAlertStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
require.JSONEq(t, `
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"alerts": [{
|
||
"labels": {
|
||
"alertname": "test_title_0",
|
||
"instance_label": "test",
|
||
"label": "test"
|
||
},
|
||
"annotations": {
|
||
"annotation": "test"
|
||
},
|
||
"state": "Recovering",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": "1.1e+00"
|
||
}]
|
||
}
|
||
}`,
|
||
string(r.Body()),
|
||
)
|
||
})
|
||
|
||
t.Run("with the inclusion of internal labels", func(t *testing.T) {
|
||
_, fakeAIM, api := setupAPI(t)
|
||
fakeAIM.GenerateAlertInstances(orgID, util.GenerateShortUID(), 2)
|
||
req, err := http.NewRequest("GET", "/api/v1/alerts?includeInternalLabels=true", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}}
|
||
|
||
r := api.RouteGetAlertStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
require.JSONEq(t, `
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"alerts": [{
|
||
"labels": {
|
||
"__alert_rule_namespace_uid__": "test_namespace_uid",
|
||
"__alert_rule_uid__": "test_alert_rule_uid_0",
|
||
"alertname": "test_title_0",
|
||
"instance_label": "test",
|
||
"label": "test"
|
||
},
|
||
"annotations": {
|
||
"annotation": "test"
|
||
},
|
||
"state": "Normal",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": ""
|
||
}, {
|
||
"labels": {
|
||
"__alert_rule_namespace_uid__": "test_namespace_uid",
|
||
"__alert_rule_uid__": "test_alert_rule_uid_1",
|
||
"alertname": "test_title_1",
|
||
"instance_label": "test",
|
||
"label": "test"
|
||
},
|
||
"annotations": {
|
||
"annotation": "test"
|
||
},
|
||
"state": "Normal",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": ""
|
||
}]
|
||
}
|
||
}`, string(r.Body()))
|
||
})
|
||
}
|
||
|
||
func withAlertingState() forEachState {
|
||
return func(s *state.State) *state.State {
|
||
s.State = eval.Alerting
|
||
s.LatestResult = &state.Evaluation{
|
||
EvaluationState: eval.Alerting,
|
||
EvaluationTime: timeNow(),
|
||
Values: map[string]float64{"B": float64(1.1)},
|
||
Condition: "B",
|
||
}
|
||
return s
|
||
}
|
||
}
|
||
|
||
func withRecoveringState() forEachState {
|
||
return func(s *state.State) *state.State {
|
||
s.State = eval.Recovering
|
||
s.LatestResult = &state.Evaluation{
|
||
EvaluationState: eval.Alerting,
|
||
EvaluationTime: timeNow(),
|
||
Values: map[string]float64{"B": float64(1.1)},
|
||
Condition: "B",
|
||
}
|
||
return s
|
||
}
|
||
}
|
||
|
||
func withAlertingErrorState() forEachState {
|
||
return func(s *state.State) *state.State {
|
||
s.SetAlerting("", timeNow(), timeNow().Add(5*time.Minute))
|
||
s.Error = errors.New("this is an error")
|
||
return s
|
||
}
|
||
}
|
||
|
||
func withErrorState() forEachState {
|
||
return func(s *state.State) *state.State {
|
||
s.SetError(errors.New("this is an error"), timeNow(), timeNow().Add(5*time.Minute))
|
||
return s
|
||
}
|
||
}
|
||
|
||
func withNoDataState() forEachState {
|
||
return func(s *state.State) *state.State {
|
||
s.SetNoData("no data returned", timeNow(), timeNow().Add(5*time.Minute))
|
||
return s
|
||
}
|
||
}
|
||
|
||
func withLabels(labels data.Labels) forEachState {
|
||
return func(s *state.State) *state.State {
|
||
for k, v := range labels {
|
||
s.Labels[k] = v
|
||
}
|
||
return s
|
||
}
|
||
}
|
||
|
||
//nolint:gocyclo
|
||
func TestRouteGetRuleStatuses(t *testing.T) {
|
||
timeNow = func() time.Time { return time.Date(2022, 3, 10, 14, 0, 0, 0, time.UTC) }
|
||
orgID := int64(1)
|
||
gen := ngmodels.RuleGen
|
||
gen = gen.With(gen.WithOrgID(orgID))
|
||
queryPermissions := map[int64]map[string][]string{1: {datasources.ActionQuery: {datasources.ScopeAll}}}
|
||
|
||
req, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions}}
|
||
|
||
t.Run("with no rules", func(t *testing.T) {
|
||
_, _, api := setupAPI(t)
|
||
r := api.RouteGetRuleStatuses(c)
|
||
|
||
require.JSONEq(t, `
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"groups": []
|
||
}
|
||
}
|
||
`, string(r.Body()))
|
||
})
|
||
|
||
t.Run("with a rule that only has one query", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), gen.WithNoNotificationSettings(), gen.WithIsPaused(false))
|
||
folder := fakeStore.Folders[orgID][0]
|
||
|
||
r := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
require.JSONEq(t, fmt.Sprintf(`
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"groups": [{
|
||
"name": "rule-group",
|
||
"file": "%s",
|
||
"folderUid": "namespaceUID",
|
||
"rules": [{
|
||
"state": "inactive",
|
||
"name": "AlwaysFiring",
|
||
"folderUid": "namespaceUID",
|
||
"uid": "RuleUID",
|
||
"query": "vector(1)",
|
||
"queriedDatasourceUIDs": ["AUID"],
|
||
"alerts": [{
|
||
"labels": {
|
||
"job": "prometheus"
|
||
},
|
||
"annotations": {
|
||
"severity": "critical"
|
||
},
|
||
"state": "Normal",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": ""
|
||
}],
|
||
"totals": {
|
||
"normal": 1
|
||
},
|
||
"totalsFiltered": {
|
||
"normal": 1
|
||
},
|
||
"labels": {
|
||
"__a_private_label_on_the_rule__": "a_value"
|
||
},
|
||
"health": "ok",
|
||
"isPaused": false,
|
||
"type": "alerting",
|
||
"lastEvaluation": "2022-03-10T14:01:00Z",
|
||
"duration": 180,
|
||
"keepFiringFor": 10,
|
||
"evaluationTime": 60
|
||
}],
|
||
"totals": {
|
||
"inactive": 1
|
||
},
|
||
"interval": 60,
|
||
"lastEvaluation": "2022-03-10T14:01:00Z",
|
||
"evaluationTime": 60
|
||
}],
|
||
"totals": {
|
||
"inactive": 1
|
||
}
|
||
}
|
||
}
|
||
`, folder.Fullpath), string(r.Body()))
|
||
})
|
||
|
||
t.Run("with a rule that is paused", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), gen.WithNoNotificationSettings(), gen.WithIsPaused(true))
|
||
folder := fakeStore.Folders[orgID][0]
|
||
|
||
r := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
require.JSONEq(t, fmt.Sprintf(`
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"groups": [{
|
||
"name": "rule-group",
|
||
"file": "%s",
|
||
"folderUid": "namespaceUID",
|
||
"rules": [{
|
||
"state": "inactive",
|
||
"name": "AlwaysFiring",
|
||
"folderUid": "namespaceUID",
|
||
"uid": "RuleUID",
|
||
"query": "vector(1)",
|
||
"queriedDatasourceUIDs": ["AUID"],
|
||
"alerts": [{
|
||
"labels": {
|
||
"job": "prometheus"
|
||
},
|
||
"annotations": {
|
||
"severity": "critical"
|
||
},
|
||
"state": "Normal",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": ""
|
||
}],
|
||
"totals": {
|
||
"normal": 1
|
||
},
|
||
"totalsFiltered": {
|
||
"normal": 1
|
||
},
|
||
"labels": {
|
||
"__a_private_label_on_the_rule__": "a_value"
|
||
},
|
||
"health": "ok",
|
||
"isPaused": true,
|
||
"type": "alerting",
|
||
"lastEvaluation": "2022-03-10T14:01:00Z",
|
||
"duration": 180,
|
||
"keepFiringFor": 10,
|
||
"evaluationTime": 60
|
||
}],
|
||
"totals": {
|
||
"inactive": 1
|
||
},
|
||
"interval": 60,
|
||
"lastEvaluation": "2022-03-10T14:01:00Z",
|
||
"evaluationTime": 60
|
||
}],
|
||
"totals": {
|
||
"inactive": 1
|
||
}
|
||
}
|
||
}
|
||
`, folder.Fullpath), string(r.Body()))
|
||
})
|
||
|
||
t.Run("with a rule that has notification settings", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
notificationSettings := ngmodels.NotificationSettings{
|
||
Receiver: "test-receiver",
|
||
GroupBy: []string{"job"},
|
||
}
|
||
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), gen.WithNotificationSettings(notificationSettings), gen.WithIsPaused(false))
|
||
r := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(r.Body(), &res))
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
require.Len(t, res.Data.RuleGroups[0].Rules, 1)
|
||
require.NotNil(t, res.Data.RuleGroups[0].Rules[0].NotificationSettings)
|
||
require.Equal(t, notificationSettings.Receiver, res.Data.RuleGroups[0].Rules[0].NotificationSettings.Receiver)
|
||
})
|
||
|
||
t.Run("with the inclusion of internal Labels", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), gen.WithNoNotificationSettings(), gen.WithIsPaused(false))
|
||
folder := fakeStore.Folders[orgID][0]
|
||
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?includeInternalLabels=true", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions}}
|
||
|
||
r := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
require.JSONEq(t, fmt.Sprintf(`
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"groups": [{
|
||
"name": "rule-group",
|
||
"file": "%s",
|
||
"folderUid": "namespaceUID",
|
||
"rules": [{
|
||
"state": "inactive",
|
||
"name": "AlwaysFiring",
|
||
"query": "vector(1)",
|
||
"queriedDatasourceUIDs": ["AUID"],
|
||
"folderUid": "namespaceUID",
|
||
"uid": "RuleUID",
|
||
"alerts": [{
|
||
"labels": {
|
||
"job": "prometheus",
|
||
"__alert_rule_namespace_uid__": "test_namespace_uid",
|
||
"__alert_rule_uid__": "test_alert_rule_uid_0"
|
||
},
|
||
"annotations": {
|
||
"severity": "critical"
|
||
},
|
||
"state": "Normal",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": ""
|
||
}],
|
||
"totals": {
|
||
"normal": 1
|
||
},
|
||
"totalsFiltered": {
|
||
"normal": 1
|
||
},
|
||
"labels": {
|
||
"__a_private_label_on_the_rule__": "a_value",
|
||
"__alert_rule_uid__": "RuleUID"
|
||
},
|
||
"health": "ok",
|
||
"isPaused": false,
|
||
"type": "alerting",
|
||
"lastEvaluation": "2022-03-10T14:01:00Z",
|
||
"duration": 180,
|
||
"keepFiringFor": 10,
|
||
"evaluationTime": 60
|
||
}],
|
||
"totals": {
|
||
"inactive": 1
|
||
},
|
||
"interval": 60,
|
||
"lastEvaluation": "2022-03-10T14:01:00Z",
|
||
"evaluationTime": 60
|
||
}],
|
||
"totals": {
|
||
"inactive": 1
|
||
}
|
||
}
|
||
}
|
||
`, folder.Fullpath), string(r.Body()))
|
||
})
|
||
|
||
t.Run("with a rule that has multiple queries", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withExpressionsMultiQuery(), gen.WithNoNotificationSettings(), gen.WithIsPaused(false))
|
||
folder := fakeStore.Folders[orgID][0]
|
||
|
||
r := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
require.JSONEq(t, fmt.Sprintf(`
|
||
{
|
||
"status": "success",
|
||
"data": {
|
||
"groups": [{
|
||
"name": "rule-group",
|
||
"file": "%s",
|
||
"folderUid": "namespaceUID",
|
||
"rules": [{
|
||
"state": "inactive",
|
||
"name": "AlwaysFiring",
|
||
"query": "vector(1) | vector(1)",
|
||
"queriedDatasourceUIDs": ["AUID", "BUID"],
|
||
"folderUid": "namespaceUID",
|
||
"uid": "RuleUID",
|
||
"alerts": [{
|
||
"labels": {
|
||
"job": "prometheus"
|
||
},
|
||
"annotations": {
|
||
"severity": "critical"
|
||
},
|
||
"state": "Normal",
|
||
"activeAt": "0001-01-01T00:00:00Z",
|
||
"value": ""
|
||
}],
|
||
"totals": {
|
||
"normal": 1
|
||
},
|
||
"totalsFiltered": {
|
||
"normal": 1
|
||
},
|
||
"labels": {
|
||
"__a_private_label_on_the_rule__": "a_value"
|
||
},
|
||
"health": "ok",
|
||
"isPaused": false,
|
||
"type": "alerting",
|
||
"lastEvaluation": "2022-03-10T14:01:00Z",
|
||
"duration": 180,
|
||
"keepFiringFor": 10,
|
||
"evaluationTime": 60
|
||
}],
|
||
"totals": {
|
||
"inactive": 1
|
||
},
|
||
"interval": 60,
|
||
"lastEvaluation": "2022-03-10T14:01:00Z",
|
||
"evaluationTime": 60
|
||
}],
|
||
"totals": {
|
||
"inactive": 1
|
||
}
|
||
}
|
||
}
|
||
`, folder.Fullpath), string(r.Body()))
|
||
})
|
||
|
||
t.Run("with a recovering alert", func(t *testing.T) {
|
||
gen := ngmodels.RuleGen
|
||
|
||
t.Run("when it is the only alert", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
rule := gen.With(gen.WithOrgID(orgID), asFixture(), withClassicConditionSingleQuery()).GenerateRef()
|
||
fakeAIM.GenerateAlertInstances(1, rule.UID, 1, withRecoveringState())
|
||
fakeStore.PutRule(context.Background(), rule)
|
||
|
||
r := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(r.Body(), &res))
|
||
|
||
// There should be 1 recovering rule
|
||
require.Equal(t, map[string]int64{"recovering": 1}, res.Data.Totals)
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Equal(t, "recovering", rg.Rules[0].State)
|
||
|
||
// The rule should have one recovering alert
|
||
require.Equal(t, map[string]int64{"recovering": 1}, rg.Rules[0].Totals)
|
||
require.Equal(t, map[string]int64{"recovering": 1}, rg.Rules[0].TotalsFiltered)
|
||
require.Len(t, rg.Rules[0].Alerts, 1)
|
||
require.Equal(t, "Recovering", rg.Rules[0].Alerts[0].State)
|
||
})
|
||
|
||
t.Run("when the rule has also a firing alert", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
rule := gen.With(gen.WithOrgID(orgID), asFixture(), withClassicConditionSingleQuery()).GenerateRef()
|
||
fakeAIM.GenerateAlertInstances(orgID, rule.UID, 1, withRecoveringState())
|
||
fakeAIM.GenerateAlertInstances(orgID, rule.UID, 1, withAlertingState())
|
||
fakeStore.PutRule(context.Background(), rule)
|
||
|
||
r := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(r.Body(), &res))
|
||
|
||
// There should be 1 firing rule
|
||
require.Equal(t, map[string]int64{"firing": 1}, res.Data.Totals)
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Equal(t, "firing", rg.Rules[0].State)
|
||
|
||
// The rule should have one firing and one recovering alert
|
||
require.Equal(t, map[string]int64{"alerting": 1, "recovering": 1}, rg.Rules[0].Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "recovering": 1}, rg.Rules[0].TotalsFiltered)
|
||
require.Len(t, rg.Rules[0].Alerts, 2)
|
||
alertStates := []string{rg.Rules[0].Alerts[0].State, rg.Rules[0].Alerts[1].State}
|
||
require.ElementsMatch(t, alertStates, []string{"Alerting", "Recovering"})
|
||
})
|
||
|
||
t.Run("filtered by recovering state", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
groupKey := ngmodels.GenerateGroupKey(orgID)
|
||
recoveringRule := gen.With(gen.WithOrgID(orgID), gen.WithGroupKey(groupKey), withClassicConditionSingleQuery()).GenerateRef()
|
||
alertingRule := gen.With(gen.WithOrgID(orgID), gen.WithGroupKey(groupKey), withClassicConditionSingleQuery()).GenerateRef()
|
||
fakeAIM.GenerateAlertInstances(orgID, recoveringRule.UID, 1, withRecoveringState())
|
||
fakeAIM.GenerateAlertInstances(orgID, alertingRule.UID, 1, withAlertingState())
|
||
fakeStore.PutRule(context.Background(), recoveringRule)
|
||
fakeStore.PutRule(context.Background(), alertingRule)
|
||
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?state=recovering", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
r := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, r.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(r.Body(), &res))
|
||
|
||
// global totals aren't filtered
|
||
require.Equal(t, map[string]int64{"recovering": 1, "firing": 1}, res.Data.Totals)
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Equal(t, "recovering", rg.Rules[0].State)
|
||
|
||
// The rule should have one recovering alert
|
||
require.Equal(t, map[string]int64{"recovering": 1}, rg.Rules[0].Totals)
|
||
require.Equal(t, map[string]int64{"recovering": 1}, rg.Rules[0].TotalsFiltered)
|
||
require.Len(t, rg.Rules[0].Alerts, 1)
|
||
require.Equal(t, "Recovering", rg.Rules[0].Alerts[0].State)
|
||
})
|
||
})
|
||
|
||
t.Run("with many rules in a group", func(t *testing.T) {
|
||
t.Run("should return sorted", func(t *testing.T) {
|
||
ruleStore := fakes.NewRuleStore(t)
|
||
fakeAIM := NewFakeAlertInstanceManager(t)
|
||
fakeSch := newFakeSchedulerReader(t).setupStates(fakeAIM)
|
||
groupKey := ngmodels.GenerateGroupKey(orgID)
|
||
gen := ngmodels.RuleGen
|
||
rules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex()).GenerateManyRef(5, 10)
|
||
ruleStore.PutRule(context.Background(), rules...)
|
||
|
||
api := NewPrometheusSrv(
|
||
log.NewNopLogger(),
|
||
fakeAIM,
|
||
fakeSch,
|
||
ruleStore,
|
||
&fakeRuleAccessControlService{},
|
||
fakes.NewFakeProvisioningStore(),
|
||
)
|
||
|
||
response := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, response.Status())
|
||
result := &apimodels.RuleResponse{}
|
||
require.NoError(t, json.Unmarshal(response.Body(), result))
|
||
|
||
ngmodels.RulesGroup(rules).SortByGroupIndex()
|
||
|
||
require.Len(t, result.Data.RuleGroups, 1)
|
||
group := result.Data.RuleGroups[0]
|
||
require.Equal(t, groupKey.RuleGroup, group.Name)
|
||
require.Len(t, group.Rules, len(rules))
|
||
for i, actual := range group.Rules {
|
||
expected := rules[i]
|
||
if actual.Name != expected.Title {
|
||
var actualNames []string
|
||
var expectedNames []string
|
||
for _, rule := range group.Rules {
|
||
actualNames = append(actualNames, rule.Name)
|
||
}
|
||
for _, rule := range rules {
|
||
expectedNames = append(expectedNames, rule.Title)
|
||
}
|
||
require.Fail(t, fmt.Sprintf("rules are not sorted by group index. Expected: %v. Actual: %v", expectedNames, actualNames))
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
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 := NewPrometheusSrv(
|
||
log.NewNopLogger(),
|
||
fakeAIM,
|
||
newFakeSchedulerReader(t).setupStates(fakeAIM),
|
||
ruleStore,
|
||
accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures())),
|
||
fakes.NewFakeProvisioningStore(),
|
||
)
|
||
|
||
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 := ruleStore.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("should only return rules with given rule_uid list", func(t *testing.T) {
|
||
expectedRuleInGroup1 := rulesInGroup1[0]
|
||
expectedRuleInGroup3 := rulesInGroup3[1]
|
||
|
||
r, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?rule_uid=%s&rule_uid=%s", expectedRuleInGroup1.UID, expectedRuleInGroup3.UID), 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, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool {
|
||
return rg.Name == "rule-group-1"
|
||
}))
|
||
require.True(t, 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-1" {
|
||
require.Equal(t, expectedRuleInGroup1.UID, result.Data.RuleGroups[0].Rules[0].UID)
|
||
require.Equal(t, expectedRuleInGroup3.UID, result.Data.RuleGroups[1].Rules[0].UID)
|
||
} else {
|
||
require.Equal(t, expectedRuleInGroup1.UID, result.Data.RuleGroups[1].Rules[0].UID)
|
||
require.Equal(t, expectedRuleInGroup3.UID, result.Data.RuleGroups[0].Rules[0].UID)
|
||
}
|
||
})
|
||
})
|
||
|
||
t.Run("when requesting rules with pagination", func(t *testing.T) {
|
||
ruleStore := fakes.NewRuleStore(t)
|
||
fakeAIM := NewFakeAlertInstanceManager(t)
|
||
|
||
// Generate 9 rule groups across 3 namespaces
|
||
// Added in reverse order so we can check that
|
||
// they are sorted when returned
|
||
allRules := make([]*ngmodels.AlertRule, 0, 9)
|
||
for i := 8; i >= 0; i-- {
|
||
rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
|
||
RuleGroup: fmt.Sprintf("rule_group_%d", i),
|
||
NamespaceUID: fmt.Sprintf("namespace_%d", i/9),
|
||
OrgID: orgID,
|
||
})).GenerateManyRef(1)
|
||
|
||
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 all groups when not specifying max_groups query param", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules", 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, 9)
|
||
require.NotZero(t, len(result.Data.Totals))
|
||
for i := 0; i < 9; i++ {
|
||
folder, err := ruleStore.GetNamespaceByUID(context.Background(), fmt.Sprintf("namespace_%d", i/9), orgID, user)
|
||
require.NoError(t, err)
|
||
require.Equal(t, folder.Fullpath, result.Data.RuleGroups[i].File)
|
||
require.Equal(t, fmt.Sprintf("rule_group_%d", i), result.Data.RuleGroups[i].Name)
|
||
}
|
||
})
|
||
|
||
t.Run("should return group_limit number of groups in each call", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?group_limit=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))
|
||
|
||
returnedGroups := make([]apimodels.RuleGroup, 0, len(allRules))
|
||
|
||
require.Len(t, result.Data.RuleGroups, 2)
|
||
require.Len(t, result.Data.Totals, 0)
|
||
returnedGroups = append(returnedGroups, result.Data.RuleGroups...)
|
||
require.NotEmpty(t, result.Data.NextToken)
|
||
token := result.Data.NextToken
|
||
|
||
for i := 0; i < 3; i++ {
|
||
r, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?group_limit=2&group_next_token=%s", token), 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.Len(t, result.Data.Totals, 0)
|
||
returnedGroups = append(returnedGroups, result.Data.RuleGroups...)
|
||
require.NotEmpty(t, result.Data.NextToken)
|
||
token = result.Data.NextToken
|
||
}
|
||
|
||
// Final page should only return a single group and no token
|
||
r, err = http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?group_limit=2&group_next_token=%s", token), 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)
|
||
require.Len(t, result.Data.Totals, 0)
|
||
returnedGroups = append(returnedGroups, result.Data.RuleGroups...)
|
||
require.Empty(t, result.Data.NextToken)
|
||
|
||
for i := 0; i < 9; i++ {
|
||
folder, err := ruleStore.GetNamespaceByUID(context.Background(), fmt.Sprintf("namespace_%d", i/9), orgID, user)
|
||
require.NoError(t, err)
|
||
require.Equal(t, folder.Fullpath, returnedGroups[i].File)
|
||
require.Equal(t, fmt.Sprintf("rule_group_%d", i), returnedGroups[i].Name)
|
||
}
|
||
})
|
||
|
||
t.Run("bad token should return first group_limit results", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?group_limit=1&group_next_token=foobar", 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)
|
||
require.Len(t, result.Data.Totals, 0)
|
||
require.NotEmpty(t, result.Data.NextToken)
|
||
|
||
folder, err := ruleStore.GetNamespaceByUID(context.Background(), "namespace_0", orgID, user)
|
||
require.NoError(t, err)
|
||
require.Equal(t, folder.Fullpath, result.Data.RuleGroups[0].File)
|
||
require.Equal(t, "rule_group_0", result.Data.RuleGroups[0].Name)
|
||
})
|
||
|
||
t.Run("should return nothing when using group_limit=0", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?group_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 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)
|
||
fakeAIM := NewFakeAlertInstanceManager(t)
|
||
|
||
rules := gen.GenerateManyRef(2, 6)
|
||
ruleStore.PutRule(context.Background(), rules...)
|
||
ruleStore.PutRule(context.Background(), gen.GenerateManyRef(2, 6)...)
|
||
|
||
api := NewPrometheusSrv(
|
||
log.NewNopLogger(),
|
||
fakeAIM,
|
||
newFakeSchedulerReader(t).setupStates(fakeAIM),
|
||
ruleStore,
|
||
accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures())),
|
||
fakes.NewFakeProvisioningStore(),
|
||
)
|
||
|
||
c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: createPermissionsForRules(rules, orgID)}}
|
||
|
||
response := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, response.Status())
|
||
result := &apimodels.RuleResponse{}
|
||
require.NoError(t, json.Unmarshal(response.Body(), result))
|
||
for _, group := range result.Data.RuleGroups {
|
||
grouploop:
|
||
for _, rule := range group.Rules {
|
||
for i, expected := range rules {
|
||
if rule.Name == expected.Title && group.Name == expected.RuleGroup {
|
||
rules = append(rules[:i], rules[i+1:]...)
|
||
continue grouploop
|
||
}
|
||
}
|
||
assert.Failf(t, "rule %s in a group %s was not found in expected", rule.Name, group.Name)
|
||
}
|
||
}
|
||
assert.Emptyf(t, rules, "not all expected rules were returned")
|
||
})
|
||
})
|
||
|
||
t.Run("test totals are expected", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
// Create rules in the same Rule Group to keep assertions simple
|
||
rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
|
||
RuleGroup: "Rule-Group-1",
|
||
NamespaceUID: "Folder-1",
|
||
OrgID: orgID,
|
||
})).GenerateManyRef(3)
|
||
// Need to sort these so we add alerts to the rules as ordered in the response
|
||
ngmodels.AlertRulesBy(ngmodels.AlertRulesByIndex).Sort(rules)
|
||
// The last two rules will have errors, however the first will be alerting
|
||
// while the second one will have a DatasourceError alert.
|
||
rules[1].ExecErrState = ngmodels.AlertingErrState
|
||
rules[2].ExecErrState = ngmodels.ErrorErrState
|
||
fakeStore.PutRule(context.Background(), rules...)
|
||
|
||
// create a normal and alerting state for the first rule
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1)
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1, withAlertingState())
|
||
// create an error state for the last two rules
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[1].UID, 1, withAlertingErrorState())
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[2].UID, 1, withErrorState())
|
||
|
||
r, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Even though there are just 3 rules, the totals should show two firing rules,
|
||
// one inactive rules and two errors
|
||
require.Equal(t, map[string]int64{"firing": 2, "inactive": 1, "error": 2}, res.Data.Totals)
|
||
// There should be 1 Rule Group that contains all rules
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 3)
|
||
|
||
// The first rule should have an alerting and normal alert
|
||
r1 := rg.Rules[0]
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, r1.Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, r1.TotalsFiltered)
|
||
require.Len(t, r1.Alerts, 2)
|
||
// The second rule should have an alerting alert
|
||
r2 := rg.Rules[1]
|
||
require.Equal(t, map[string]int64{"alerting": 1, "error": 1}, r2.Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "error": 1}, r2.TotalsFiltered)
|
||
require.Len(t, r2.Alerts, 1)
|
||
// The last rule should have an error alert
|
||
r3 := rg.Rules[2]
|
||
require.Equal(t, map[string]int64{"error": 1}, r3.Totals)
|
||
require.Equal(t, map[string]int64{"error": 1}, r3.TotalsFiltered)
|
||
require.Len(t, r3.Alerts, 1)
|
||
})
|
||
|
||
t.Run("test time of first firing alert", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
// Create rules in the same Rule Group to keep assertions simple
|
||
rules := gen.GenerateManyRef(1)
|
||
fakeStore.PutRule(context.Background(), rules...)
|
||
|
||
getRuleResponse := func() apimodels.RuleResponse {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
return res
|
||
}
|
||
|
||
// no alerts so timestamp should be nil
|
||
res := getRuleResponse()
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Nil(t, rg.Rules[0].ActiveAt)
|
||
|
||
// create a normal alert, the timestamp should still be nil
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1)
|
||
res = getRuleResponse()
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg = res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Nil(t, rg.Rules[0].ActiveAt)
|
||
|
||
// create a firing alert, the timestamp should be non-nil
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1, withAlertingState())
|
||
res = getRuleResponse()
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg = res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.NotNil(t, rg.Rules[0].ActiveAt)
|
||
|
||
lastActiveAt := rg.Rules[0].ActiveAt
|
||
// create a second firing alert, the timestamp of first firing alert should be the same
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1, withAlertingState())
|
||
res = getRuleResponse()
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg = res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Equal(t, lastActiveAt, rg.Rules[0].ActiveAt)
|
||
})
|
||
|
||
t.Run("test with limit on Rule Groups", func(t *testing.T) {
|
||
fakeStore, _, api := setupAPI(t)
|
||
|
||
rules := gen.GenerateManyRef(2)
|
||
fakeStore.PutRule(context.Background(), rules...)
|
||
|
||
t.Run("first without limit", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be 2 inactive rules across all Rule Groups
|
||
require.Equal(t, map[string]int64{"inactive": 2}, res.Data.Totals)
|
||
require.Len(t, res.Data.RuleGroups, 2)
|
||
for _, rg := range res.Data.RuleGroups {
|
||
// Each Rule Group should have 1 inactive rule
|
||
require.Equal(t, map[string]int64{"inactive": 1}, rg.Totals)
|
||
require.Len(t, rg.Rules, 1)
|
||
}
|
||
})
|
||
})
|
||
|
||
t.Run("test with limit rules", func(t *testing.T) {
|
||
fakeStore, _, api := setupAPI(t)
|
||
rules := gen.With(gen.WithGroupName("Rule-Group-1")).GenerateManyRef(2)
|
||
fakeStore.PutRule(context.Background(), rules...)
|
||
|
||
t.Run("first without limit", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be 2 inactive rules across all Rule Groups
|
||
require.Equal(t, map[string]int64{"inactive": 2}, res.Data.Totals)
|
||
require.Len(t, res.Data.RuleGroups, 2)
|
||
for _, rg := range res.Data.RuleGroups {
|
||
// Each Rule Group should have 1 inactive rule
|
||
require.Equal(t, map[string]int64{"inactive": 1}, rg.Totals)
|
||
require.Len(t, rg.Rules, 1)
|
||
}
|
||
})
|
||
|
||
t.Run("then with limit", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?limit_rules=1", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be 2 inactive rules
|
||
require.Equal(t, map[string]int64{"inactive": 2}, res.Data.Totals)
|
||
require.Len(t, res.Data.RuleGroups, 2)
|
||
// The Rule Groups should have 1 inactive rule because of the limit
|
||
rg1 := res.Data.RuleGroups[0]
|
||
require.Equal(t, map[string]int64{"inactive": 1}, rg1.Totals)
|
||
require.Len(t, rg1.Rules, 1)
|
||
rg2 := res.Data.RuleGroups[1]
|
||
require.Equal(t, map[string]int64{"inactive": 1}, rg2.Totals)
|
||
require.Len(t, rg2.Rules, 1)
|
||
})
|
||
|
||
t.Run("then with limit larger than number of rules", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?limit_rules=2", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
require.Len(t, res.Data.RuleGroups, 2)
|
||
require.Len(t, res.Data.RuleGroups[0].Rules, 1)
|
||
require.Len(t, res.Data.RuleGroups[1].Rules, 1)
|
||
})
|
||
})
|
||
|
||
t.Run("test with limit alerts", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
rules := gen.With(gen.WithGroupName("Rule-Group-1")).GenerateManyRef(2)
|
||
fakeStore.PutRule(context.Background(), rules...)
|
||
// create a normal and firing alert for each rule
|
||
for _, r := range rules {
|
||
fakeAIM.GenerateAlertInstances(orgID, r.UID, 1)
|
||
fakeAIM.GenerateAlertInstances(orgID, r.UID, 1, withAlertingState())
|
||
}
|
||
|
||
t.Run("first without limit", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be 2 firing rules across all Rule Groups
|
||
require.Equal(t, map[string]int64{"firing": 2}, res.Data.Totals)
|
||
require.Len(t, res.Data.RuleGroups, 2)
|
||
for _, rg := range res.Data.RuleGroups {
|
||
// Each Rule Group should have 1 firing rule
|
||
require.Equal(t, map[string]int64{"firing": 1}, rg.Totals)
|
||
require.Len(t, rg.Rules, 1)
|
||
// Each rule should have two alerts
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, rg.Rules[0].Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, rg.Rules[0].TotalsFiltered)
|
||
}
|
||
})
|
||
|
||
t.Run("then with limits", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?limit_rules=1&limit_alerts=1", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be 2 firing rules across all Rule Groups
|
||
require.Equal(t, map[string]int64{"firing": 2}, res.Data.Totals)
|
||
rg := res.Data.RuleGroups[0]
|
||
// The Rule Group within the limit should have 1 inactive rule because of the limit
|
||
require.Equal(t, map[string]int64{"firing": 1}, rg.Totals)
|
||
require.Len(t, rg.Rules, 1)
|
||
rule := rg.Rules[0]
|
||
// The rule should have two alerts, but just one should be returned
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, rule.Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, rule.TotalsFiltered)
|
||
require.Len(t, rule.Alerts, 1)
|
||
// Firing alerts should have precedence over normal alerts
|
||
require.Equal(t, "Alerting", rule.Alerts[0].State)
|
||
})
|
||
|
||
t.Run("then with limit larger than number of alerts", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?limit_rules=1&limit_alerts=3", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
require.Len(t, res.Data.RuleGroups, 2)
|
||
require.Len(t, res.Data.RuleGroups[0].Rules, 1)
|
||
require.Len(t, res.Data.RuleGroups[0].Rules[0].Alerts, 2)
|
||
require.Len(t, res.Data.RuleGroups[1].Rules, 1)
|
||
require.Len(t, res.Data.RuleGroups[1].Rules[0].Alerts, 2)
|
||
})
|
||
})
|
||
|
||
t.Run("test with filters on state", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
// create rules in the same Rule Group to keep assertions simple
|
||
rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
|
||
NamespaceUID: "Folder-1",
|
||
RuleGroup: "Rule-Group-1",
|
||
OrgID: orgID,
|
||
})).GenerateManyRef(3)
|
||
// Need to sort these so we add alerts to the rules as ordered in the response
|
||
ngmodels.AlertRulesBy(ngmodels.AlertRulesByIndex).Sort(rules)
|
||
// The last two rules will have errors, however the first will be alerting
|
||
// while the second one will have a DatasourceError alert.
|
||
rules[1].ExecErrState = ngmodels.AlertingErrState
|
||
rules[2].ExecErrState = ngmodels.ErrorErrState
|
||
fakeStore.PutRule(context.Background(), rules...)
|
||
|
||
// create a normal and alerting state for the first rule
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1)
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1, withAlertingState())
|
||
// create an error state for the last two rules
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[1].UID, 1, withAlertingErrorState())
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[2].UID, 1, withErrorState())
|
||
|
||
t.Run("invalid state returns 400 Bad Request", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?state=unknown", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusBadRequest, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
require.Equal(t, "unknown state 'unknown'", res.Error)
|
||
})
|
||
|
||
t.Run("first without filters", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be 2 firing rules, 1 inactive rule, and 2 with errors
|
||
require.Equal(t, map[string]int64{"firing": 2, "inactive": 1, "error": 2}, res.Data.Totals)
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 3)
|
||
|
||
// The first two rules should be firing and the last should be inactive
|
||
require.Equal(t, "firing", rg.Rules[0].State)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, rg.Rules[0].Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, rg.Rules[0].TotalsFiltered)
|
||
require.Len(t, rg.Rules[0].Alerts, 2)
|
||
require.Equal(t, "firing", rg.Rules[1].State)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "error": 1}, rg.Rules[1].Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "error": 1}, rg.Rules[1].TotalsFiltered)
|
||
require.Len(t, rg.Rules[1].Alerts, 1)
|
||
require.Equal(t, "inactive", rg.Rules[2].State)
|
||
require.Equal(t, map[string]int64{"error": 1}, rg.Rules[2].Totals)
|
||
require.Equal(t, map[string]int64{"error": 1}, rg.Rules[2].TotalsFiltered)
|
||
require.Len(t, rg.Rules[2].Alerts, 1)
|
||
})
|
||
|
||
t.Run("then with filter for firing alerts", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?state=firing", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// The totals should be the same
|
||
require.Equal(t, map[string]int64{"firing": 2, "inactive": 1, "error": 2}, res.Data.Totals)
|
||
|
||
// The inactive rules should be filtered out of the result
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 2)
|
||
|
||
// Both firing rules should be returned with their totals unchanged
|
||
require.Equal(t, "firing", rg.Rules[0].State)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, rg.Rules[0].Totals)
|
||
|
||
// After filtering the totals for normal are no longer included.
|
||
require.Equal(t, map[string]int64{"alerting": 1}, rg.Rules[0].TotalsFiltered)
|
||
// The first rule should have just 1 firing alert as the inactive alert
|
||
// has been removed by the filter for firing alerts
|
||
require.Len(t, rg.Rules[0].Alerts, 1)
|
||
|
||
require.Equal(t, "firing", rg.Rules[1].State)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "error": 1}, rg.Rules[1].Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "error": 1}, rg.Rules[1].TotalsFiltered)
|
||
require.Len(t, rg.Rules[1].Alerts, 1)
|
||
})
|
||
|
||
t.Run("then with filters for both inactive and firing alerts", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?state=inactive&state=firing", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// The totals should be the same
|
||
require.Equal(t, map[string]int64{"firing": 2, "inactive": 1, "error": 2}, res.Data.Totals)
|
||
|
||
// The number of rules returned should also be the same
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 3)
|
||
|
||
// The first two rules should be firing and the last should be inactive
|
||
require.Equal(t, "firing", rg.Rules[0].State)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, rg.Rules[0].Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "normal": 1}, rg.Rules[0].TotalsFiltered)
|
||
require.Len(t, rg.Rules[0].Alerts, 2)
|
||
require.Equal(t, "firing", rg.Rules[1].State)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "error": 1}, rg.Rules[1].Totals)
|
||
require.Equal(t, map[string]int64{"alerting": 1, "error": 1}, rg.Rules[1].TotalsFiltered)
|
||
require.Len(t, rg.Rules[1].Alerts, 1)
|
||
|
||
// The last rule should have 1 alert.
|
||
require.Equal(t, "inactive", rg.Rules[2].State)
|
||
require.Equal(t, map[string]int64{"error": 1}, rg.Rules[2].Totals)
|
||
|
||
// The TotalsFiltered for error will be 0 out as the state filter does not include error.
|
||
require.Empty(t, rg.Rules[2].TotalsFiltered)
|
||
// The error alert has been removed as the filters are inactive and firing
|
||
require.Len(t, rg.Rules[2].Alerts, 0)
|
||
})
|
||
|
||
t.Run("then with all rules filtered out, no groups returned", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?health=unknown", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
require.Len(t, res.Data.RuleGroups, 0)
|
||
})
|
||
})
|
||
|
||
t.Run("test with filters on health", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
|
||
NamespaceUID: "Folder-1",
|
||
RuleGroup: "Rule-Group-1",
|
||
OrgID: orgID,
|
||
})).GenerateManyRef(4)
|
||
ngmodels.AlertRulesBy(ngmodels.AlertRulesByIndex).Sort(rules)
|
||
// Set health states
|
||
fakeStore.PutRule(context.Background(), rules...)
|
||
|
||
// create alert instances for each rule
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1)
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[1].UID, 1, withAlertingErrorState())
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[2].UID, 1, withErrorState())
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[3].UID, 1, withNoDataState())
|
||
|
||
t.Run("invalid health returns 400 Bad Request", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?health=blah", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusBadRequest, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
require.Contains(t, res.Error, "unknown health")
|
||
})
|
||
|
||
t.Run("first without filters", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 4)
|
||
})
|
||
|
||
t.Run("then with filter for ok health", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?health=ok", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Equal(t, "ok", rg.Rules[0].Health)
|
||
})
|
||
|
||
t.Run("then with filter for error health", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?health=error", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 2)
|
||
require.Equal(t, "error", rg.Rules[0].Health)
|
||
})
|
||
|
||
t.Run("then with filter for nodata health", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?health=nodata", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Equal(t, "nodata", rg.Rules[0].Health)
|
||
})
|
||
|
||
t.Run("then with multiple health filters", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?health=ok&health=error", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 3)
|
||
healths := []string{rg.Rules[0].Health, rg.Rules[1].Health}
|
||
require.ElementsMatch(t, healths, []string{"ok", "error"})
|
||
})
|
||
|
||
t.Run("then with all rules filtered out, no groups returned", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?health=unknown", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
require.Len(t, res.Data.RuleGroups, 0)
|
||
})
|
||
})
|
||
|
||
t.Run("test with matcher on labels", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
// create two rules in the same Rule Group to keep assertions simple
|
||
rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
|
||
NamespaceUID: "Folder-1",
|
||
RuleGroup: "Rule-Group-1",
|
||
OrgID: orgID,
|
||
})).GenerateManyRef(1)
|
||
fakeStore.PutRule(context.Background(), rules...)
|
||
|
||
// create a normal and alerting state for each rule
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1,
|
||
withLabels(data.Labels{"test": "value1"}))
|
||
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1,
|
||
withLabels(data.Labels{"test": "value2"}), withAlertingState())
|
||
|
||
t.Run("invalid matchers returns 400 Bad Request", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?matcher={\"name\":\"\"}", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusBadRequest, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
require.Equal(t, "bad matcher: the name cannot be blank", res.Error)
|
||
})
|
||
|
||
t.Run("first without matchers", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Len(t, rg.Rules[0].Alerts, 2)
|
||
})
|
||
|
||
t.Run("then with single matcher", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?matcher={\"name\":\"test\",\"isEqual\":true,\"value\":\"value1\"}", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be just the alert with the label test=value1
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Len(t, rg.Rules[0].Alerts, 1)
|
||
|
||
require.Equal(t, map[string]int64{"normal": 1, "alerting": 1}, rg.Rules[0].Totals)
|
||
// There should be a totalFiltered of 1 though since the matcher matched a single instance.
|
||
require.Equal(t, map[string]int64{"normal": 1}, rg.Rules[0].TotalsFiltered)
|
||
})
|
||
|
||
t.Run("then with URL encoded regex matcher", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?matcher=%7B%22name%22:%22test%22%2C%22isEqual%22:true%2C%22isRegex%22:true%2C%22value%22:%22value%5B0-9%5D%2B%22%7D%0A", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be just the alert with the label test=value1
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Len(t, rg.Rules[0].Alerts, 2)
|
||
})
|
||
|
||
t.Run("then with multiple matchers", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?matcher={\"name\":\"alertname\",\"isEqual\":true,\"value\":\"test_title_0\"}&matcher={\"name\":\"test\",\"isEqual\":true,\"value\":\"value1\"}", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be just the alert with the label test=value1
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Len(t, rg.Rules[0].Alerts, 1)
|
||
})
|
||
|
||
t.Run("then with multiple matchers that don't match", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?matcher={\"name\":\"alertname\",\"isEqual\":true,\"value\":\"test_title_0\"}&matcher={\"name\":\"test\",\"isEqual\":true,\"value\":\"value3\"}", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should no alerts
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Len(t, rg.Rules[0].Alerts, 0)
|
||
})
|
||
|
||
t.Run("then with single matcher and limit_alerts", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?limit_alerts=0&matcher={\"name\":\"test\",\"isEqual\":true,\"value\":\"value1\"}", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// There should be no alerts since we limited to 0.
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Len(t, rg.Rules[0].Alerts, 0)
|
||
|
||
require.Equal(t, map[string]int64{"normal": 1, "alerting": 1}, rg.Rules[0].Totals)
|
||
// There should be a totalFiltered of 1 though since the matcher matched a single instance.
|
||
require.Equal(t, map[string]int64{"normal": 1}, rg.Rules[0].TotalsFiltered)
|
||
})
|
||
})
|
||
|
||
t.Run("test with a contact point filter", func(t *testing.T) {
|
||
fakeStore, _, api := setupAPI(t)
|
||
rules := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
|
||
NamespaceUID: "Folder-1",
|
||
RuleGroup: "Rule-Group-1",
|
||
OrgID: orgID,
|
||
}), gen.WithNotificationSettings(
|
||
ngmodels.NotificationSettings{
|
||
Receiver: "webhook-a",
|
||
GroupBy: []string{"alertname"},
|
||
},
|
||
)).GenerateManyRef(1)
|
||
fakeStore.PutRule(context.Background(), rules...)
|
||
|
||
t.Run("unknown receiver_name returns empty list", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?receiver_name=webhook-b", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
require.Len(t, res.Data.RuleGroups, 0)
|
||
})
|
||
t.Run("known receiver_name returns rules with that receiver", func(t *testing.T) {
|
||
r, err := http.NewRequest("GET", "/api/v1/rules?receiver_name=webhook-a", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: r},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 1)
|
||
require.Equal(t, "webhook-a", rg.Rules[0].NotificationSettings.Receiver)
|
||
})
|
||
})
|
||
|
||
t.Run("provenance as expected", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api, provStore := setupAPIFull(t)
|
||
// Rule without provenance
|
||
ruleNoProv := gen.With(gen.WithOrgID(orgID), asFixture(), withClassicConditionSingleQuery()).GenerateRef()
|
||
fakeAIM.GenerateAlertInstances(orgID, ruleNoProv.UID, 1)
|
||
fakeStore.PutRule(context.Background(), ruleNoProv)
|
||
|
||
// Rule with provenance
|
||
ruleWithProv := gen.With(gen.WithOrgID(orgID), asFixture(), withClassicConditionSingleQuery()).GenerateRef()
|
||
ruleWithProv.UID = "provRuleUID"
|
||
ruleWithProv.Title = "ProvisionedRule"
|
||
fakeAIM.GenerateAlertInstances(orgID, ruleWithProv.UID, 1)
|
||
fakeStore.PutRule(context.Background(), ruleWithProv)
|
||
|
||
// Add provenance for ruleWithProv
|
||
err := provStore.SetProvenance(context.Background(), ruleWithProv, orgID, ngmodels.ProvenanceAPI)
|
||
require.NoError(t, err)
|
||
|
||
req, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: map[int64]map[string][]string{orgID: {datasources.ActionQuery: {datasources.ScopeAll}}},
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Should have two rules in one group
|
||
require.Len(t, res.Data.RuleGroups, 1)
|
||
rg := res.Data.RuleGroups[0]
|
||
require.Len(t, rg.Rules, 2)
|
||
|
||
// Find rules by UID
|
||
var foundNoProv, foundWithProv bool
|
||
for _, rule := range rg.Rules {
|
||
switch rule.UID {
|
||
case ruleNoProv.UID:
|
||
foundNoProv = true
|
||
require.Equal(t, apimodels.Provenance(ngmodels.ProvenanceNone), rule.Provenance, "non-provisioned rule should have empty provenance")
|
||
case ruleWithProv.UID:
|
||
foundWithProv = true
|
||
require.Equal(t, apimodels.Provenance(ngmodels.ProvenanceAPI), rule.Provenance, "provisioned rule should have provenance set")
|
||
}
|
||
}
|
||
require.True(t, foundNoProv, "should find rule without provenance")
|
||
require.True(t, foundWithProv, "should find rule with provenance")
|
||
})
|
||
|
||
t.Run("filter-aware pagination", func(t *testing.T) {
|
||
createRulesWithState := func(t *testing.T, store *fakes.RuleStore, aim *fakeAlertInstanceManager,
|
||
orgID int64, numGroups int, rulesPerGroup int,
|
||
stateFunc func(groupIdx int) eval.State,
|
||
healthFunc func(groupIdx int) error,
|
||
stateMutators ...func(groupIdx int, s *state.State) *state.State) {
|
||
t.Helper()
|
||
|
||
// create folders
|
||
for i := 1; i <= numGroups; i++ {
|
||
store.Folders[orgID] = append(store.Folders[orgID], &folder.Folder{
|
||
ID: int64(i),
|
||
UID: fmt.Sprintf("ns-%d", i),
|
||
Title: fmt.Sprintf("Namespace %d", i),
|
||
Fullpath: fmt.Sprintf("/namespace-%d", i),
|
||
})
|
||
}
|
||
|
||
for i := 0; i < numGroups; i++ {
|
||
for j := 0; j < rulesPerGroup; j++ {
|
||
rule := gen.With(gen.WithOrgID(orgID), func(r *ngmodels.AlertRule) {
|
||
r.NamespaceUID = fmt.Sprintf("ns-%d", i+1)
|
||
r.RuleGroup = fmt.Sprintf("group-%d", i+1)
|
||
r.UID = fmt.Sprintf("rule-%d-%d", i+1, j+1)
|
||
}, withClassicConditionSingleQuery()).GenerateRef()
|
||
|
||
alertState := stateFunc(i)
|
||
healthErr := healthFunc(i)
|
||
|
||
aim.GenerateAlertInstances(orgID, rule.UID, 1, func(s *state.State) *state.State {
|
||
s.State = alertState
|
||
s.Error = healthErr
|
||
s.Labels = data.Labels{"test": "label"}
|
||
|
||
for _, mutator := range stateMutators {
|
||
s = mutator(i, s)
|
||
}
|
||
return s
|
||
})
|
||
store.PutRule(context.Background(), rule)
|
||
}
|
||
}
|
||
}
|
||
|
||
t.Run("state filter fetches multiple pages to fill group_limit", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 10 groups (2 rules each = 20 rules total): groups 1,3,5,7,9 firing, groups 2,4,6,8,10 normal
|
||
// Request group_limit=3 with state=firing should fetch pages until 3 firing groups collected
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2,
|
||
func(i int) eval.State {
|
||
if i%2 == 0 {
|
||
return eval.Alerting
|
||
}
|
||
return eval.Normal
|
||
},
|
||
func(i int) error { return nil })
|
||
|
||
// Request 3 groups with state=firing filter
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&group_limit=3", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Should return 3 firing groups
|
||
require.Len(t, res.Data.RuleGroups, 3)
|
||
require.Equal(t, "group-1", res.Data.RuleGroups[0].Name)
|
||
require.Equal(t, "group-3", res.Data.RuleGroups[1].Name)
|
||
require.Equal(t, "group-5", res.Data.RuleGroups[2].Name)
|
||
|
||
// Verify all have firing alerts
|
||
for _, rg := range res.Data.RuleGroups {
|
||
hasFiring := false
|
||
for _, rule := range rg.Rules {
|
||
for _, alert := range rule.Alerts {
|
||
if alert.State == eval.Alerting.String() {
|
||
hasFiring = true
|
||
}
|
||
}
|
||
}
|
||
require.True(t, hasFiring)
|
||
}
|
||
})
|
||
|
||
t.Run("state filter continues when first page has no matches", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 8 groups (2 rules each = 16 rules total): first 4 normal, last 4 firing
|
||
// Request state=firing with group_limit=2 should skip first 4 and return groups 5,6
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 8, 2,
|
||
func(i int) eval.State {
|
||
if i < 4 {
|
||
return eval.Normal // groups 1-4 normal
|
||
}
|
||
return eval.Alerting // groups 5-8 firing
|
||
},
|
||
func(i int) error { return nil })
|
||
|
||
// Request 2 firing groups - should skip past the first page of normal rules
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&group_limit=2", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Should return 2 firing groups
|
||
require.Len(t, res.Data.RuleGroups, 2)
|
||
require.Equal(t, "group-5", res.Data.RuleGroups[0].Name)
|
||
require.Equal(t, "group-6", res.Data.RuleGroups[1].Name)
|
||
})
|
||
|
||
t.Run("health filter fetches multiple pages", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 8 groups (2 rules each = 16 rules total): groups 1,3,5,7 with error health, groups 2,4,6,8 with ok health
|
||
// Request health=error with group_limit=3 should fetch pages until 3 error groups collected
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 8, 2,
|
||
func(i int) eval.State { return eval.Normal },
|
||
func(i int) error {
|
||
if i%2 == 0 {
|
||
return fmt.Errorf("evaluation error")
|
||
}
|
||
return nil
|
||
})
|
||
|
||
// Request 3 groups with health=error filter
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?health=error&group_limit=3", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Should return 3 error groups
|
||
require.Len(t, res.Data.RuleGroups, 3)
|
||
require.Equal(t, "group-1", res.Data.RuleGroups[0].Name)
|
||
require.Equal(t, "group-3", res.Data.RuleGroups[1].Name)
|
||
require.Equal(t, "group-5", res.Data.RuleGroups[2].Name)
|
||
|
||
// Verify all have error health
|
||
for _, rg := range res.Data.RuleGroups {
|
||
for _, rule := range rg.Rules {
|
||
require.Equal(t, "error", rule.Health)
|
||
}
|
||
}
|
||
})
|
||
|
||
t.Run("combined state and health filters", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 10 groups (2 rules each = 20 rules total)
|
||
// Groups 1-5: firing, groups 6-10: normal
|
||
// Groups 1,3,5,7,9: ok health, groups 2,4,6,8,10: error health
|
||
// Groups matching both filters (firing + ok): 1,3,5
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2,
|
||
func(i int) eval.State {
|
||
if i < 5 {
|
||
return eval.Alerting
|
||
}
|
||
return eval.Normal
|
||
},
|
||
func(i int) error {
|
||
if i%2 == 1 {
|
||
return fmt.Errorf("evaluation error")
|
||
}
|
||
return nil
|
||
})
|
||
|
||
// Request 3 groups with state=firing AND health=ok
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&health=ok&group_limit=3", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Should return 3 groups matching both filters
|
||
require.Len(t, res.Data.RuleGroups, 3)
|
||
require.Equal(t, "group-1", res.Data.RuleGroups[0].Name)
|
||
require.Equal(t, "group-3", res.Data.RuleGroups[1].Name)
|
||
require.Equal(t, "group-5", res.Data.RuleGroups[2].Name)
|
||
|
||
// Verify all match both criteria
|
||
for _, rg := range res.Data.RuleGroups {
|
||
for _, rule := range rg.Rules {
|
||
require.Equal(t, "ok", rule.Health)
|
||
hasFiring := false
|
||
for _, alert := range rule.Alerts {
|
||
if alert.State == eval.Alerting.String() {
|
||
hasFiring = true
|
||
}
|
||
}
|
||
require.True(t, hasFiring)
|
||
}
|
||
}
|
||
})
|
||
|
||
t.Run("rule_limit hit before group_limit", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 5 groups (3 rules each = 15 rules total)
|
||
// Request: group_limit=10, rule_limit=8
|
||
// Expected: Should return groups 1-3 (9 rules total, exceeds limit but complete group included)
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 5, 3,
|
||
func(i int) eval.State { return eval.Alerting }, // all firing
|
||
func(i int) error { return nil }) // all healthy
|
||
|
||
// Request group_limit=10, rule_limit=8 - rule limit should hit first
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?group_limit=10&rule_limit=8", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Expected behavior with rule_limit=8:
|
||
// Group 1: 3 rules (total: 3, under 8, continue)
|
||
// Group 2: 3 rules (total: 6, under 8, continue)
|
||
// Group 3: 3 rules (total: 9, exceeds 8 but we include complete group)
|
||
// Result: 3 groups, 9 rules total
|
||
totalRules := 0
|
||
for _, rg := range res.Data.RuleGroups {
|
||
t.Logf("Group %s has %d rules", rg.Name, len(rg.Rules))
|
||
totalRules += len(rg.Rules)
|
||
}
|
||
t.Logf("Total: %d rules, %d groups", totalRules, len(res.Data.RuleGroups))
|
||
require.Equal(t, 9, totalRules)
|
||
require.Equal(t, 3, len(res.Data.RuleGroups))
|
||
})
|
||
|
||
t.Run("rule_limit without group_limit", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 10 groups (2 rules each = 20 rules total)
|
||
// Request rule_limit=7 should stop after 4 groups (8 rules total)
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2,
|
||
func(i int) eval.State { return eval.Alerting },
|
||
func(i int) error { return nil })
|
||
|
||
// Request rule_limit=7 only (group_limit unlimited)
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?rule_limit=7", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Should return 4 groups (8 rules total, stops after exceeding 7)
|
||
totalRules := 0
|
||
for _, rg := range res.Data.RuleGroups {
|
||
totalRules += len(rg.Rules)
|
||
}
|
||
require.Equal(t, 8, totalRules)
|
||
require.Equal(t, 4, len(res.Data.RuleGroups))
|
||
require.NotEmpty(t, res.Data.NextToken)
|
||
})
|
||
|
||
t.Run("empty page in middle of pagination", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 15 groups (2 rules each = 30 rules total): groups 1-3 firing, 4-8 normal, 9-13 firing, 14-15 normal
|
||
// Request state=firing with group_limit=5 should skip over normal groups
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 15, 2,
|
||
func(i int) eval.State {
|
||
groupNum := i + 1
|
||
if groupNum <= 3 || (groupNum >= 9 && groupNum <= 13) {
|
||
return eval.Alerting
|
||
}
|
||
return eval.Normal
|
||
},
|
||
func(i int) error { return nil })
|
||
|
||
// Request state=firing, group_limit=5
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&group_limit=5", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Should return 5 firing groups from pages 1 and 3 (skipping empty page 2)
|
||
require.Len(t, res.Data.RuleGroups, 5)
|
||
|
||
// Verify all are firing
|
||
for _, rg := range res.Data.RuleGroups {
|
||
for _, rule := range rg.Rules {
|
||
for _, alert := range rule.Alerts {
|
||
require.Equal(t, eval.Alerting.String(), alert.State)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
t.Run("group_limit=0 returns empty response", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 1 group (2 rules) to verify group_limit=0 returns empty
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 1, 2,
|
||
func(i int) eval.State { return eval.Alerting },
|
||
func(i int) error { return nil })
|
||
|
||
// Request group_limit=0
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?group_limit=0", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
require.Len(t, res.Data.RuleGroups, 0)
|
||
require.Empty(t, res.Data.NextToken)
|
||
})
|
||
|
||
t.Run("resume with token and filters active", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 10 groups (2 rules each = 20 rules total): odd groups firing, even groups normal
|
||
// Test pagination continuation with state filter: first page returns groups 1,3 then second page returns groups 5,7
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2,
|
||
func(i int) eval.State {
|
||
if i%2 == 0 {
|
||
return eval.Alerting
|
||
}
|
||
return eval.Normal
|
||
},
|
||
func(i int) error { return nil })
|
||
|
||
// First request: state=firing, group_limit=2
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&group_limit=2", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res1 apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res1))
|
||
|
||
// Should return 2 firing groups (1, 3)
|
||
require.Len(t, res1.Data.RuleGroups, 2)
|
||
require.NotEmpty(t, res1.Data.NextToken)
|
||
|
||
// Verify both are firing
|
||
for _, rg := range res1.Data.RuleGroups {
|
||
for _, rule := range rg.Rules {
|
||
require.Equal(t, "firing", rule.State)
|
||
}
|
||
}
|
||
|
||
// Second request: resume with token AND filter still active
|
||
req2, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?state=firing&group_limit=2&group_next_token=%s", res1.Data.NextToken), nil)
|
||
require.NoError(t, err)
|
||
c2 := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req2},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp2 := api.RouteGetRuleStatuses(c2)
|
||
require.Equal(t, http.StatusOK, resp2.Status())
|
||
|
||
var res2 apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp2.Body(), &res2))
|
||
|
||
// Should return next 2 firing groups (5, 7)
|
||
require.Len(t, res2.Data.RuleGroups, 2)
|
||
require.NotEmpty(t, res2.Data.NextToken)
|
||
|
||
// Verify both are firing
|
||
for _, rg := range res2.Data.RuleGroups {
|
||
for _, rule := range rg.Rules {
|
||
require.Equal(t, "firing", rule.State)
|
||
}
|
||
}
|
||
|
||
// Verify we got different groups than first request
|
||
firstGroupNames := make(map[string]bool)
|
||
for _, rg := range res1.Data.RuleGroups {
|
||
firstGroupNames[rg.Name] = true
|
||
}
|
||
for _, rg := range res2.Data.RuleGroups {
|
||
require.False(t, firstGroupNames[rg.Name])
|
||
}
|
||
})
|
||
|
||
t.Run("rule_limit with state filter", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 10 groups (2 rules each = 20 rules total), alternating firing/normal
|
||
// Firing groups: 1,3,5,7,9 (5 groups × 2 rules = 10 firing rules)
|
||
// Request state=firing with rule_limit=7 should return groups 1,3,5,7 (8 rules)
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2,
|
||
func(i int) eval.State {
|
||
if i%2 == 0 {
|
||
return eval.Alerting
|
||
}
|
||
return eval.Normal
|
||
},
|
||
func(i int) error { return nil })
|
||
|
||
// Request state=firing, rule_limit=7
|
||
// Should fetch multiple pages to accumulate 7+ firing rules
|
||
// Groups 1, 3, 5 = 6 rules (under limit)
|
||
// Group 7 = +2 rules = 8 total (exceeds 7, but we include full group)
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&rule_limit=7", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Count total firing rules returned
|
||
totalRules := 0
|
||
for _, rg := range res.Data.RuleGroups {
|
||
for _, rule := range rg.Rules {
|
||
require.Equal(t, "firing", rule.State)
|
||
totalRules++
|
||
}
|
||
}
|
||
|
||
// Should return 4 firing groups (1,3,5,7) with 8 rules total
|
||
require.Equal(t, 8, totalRules)
|
||
require.Equal(t, 4, len(res.Data.RuleGroups))
|
||
require.NotEmpty(t, res.Data.NextToken)
|
||
})
|
||
|
||
t.Run("rule_limit with health filter", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create 8 groups (3 rules each = 24 rules total), alternating ok/error health
|
||
// Error groups: 1,3,5,7 (4 groups × 3 rules = 12 error rules)
|
||
// Request health=error with rule_limit=8 should return groups 1,3,5 (9 rules)
|
||
createRulesWithState(t, fakeStore, fakeAIM, orgID, 8, 3,
|
||
func(i int) eval.State { return eval.Normal },
|
||
func(i int) error {
|
||
if i%2 == 0 {
|
||
return fmt.Errorf("evaluation error")
|
||
}
|
||
return nil
|
||
})
|
||
|
||
// Request health=error, rule_limit=8
|
||
// Should fetch multiple pages to accumulate 8+ error rules
|
||
// Groups 1, 3 = 6 rules (under limit)
|
||
// Group 5 = +3 rules = 9 total (exceeds 8, but we include full group)
|
||
req, err := http.NewRequest("GET", "/api/v1/rules?health=error&rule_limit=8", nil)
|
||
require.NoError(t, err)
|
||
c := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: queryPermissions,
|
||
},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(c)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
|
||
// Count total error rules returned
|
||
totalRules := 0
|
||
for _, rg := range res.Data.RuleGroups {
|
||
for _, rule := range rg.Rules {
|
||
require.Equal(t, "error", rule.Health)
|
||
totalRules++
|
||
}
|
||
}
|
||
|
||
// Should return 3 error groups (1,3,5) with 9 rules total
|
||
require.Equal(t, 9, totalRules)
|
||
require.Equal(t, 3, len(res.Data.RuleGroups))
|
||
require.NotEmpty(t, res.Data.NextToken)
|
||
})
|
||
})
|
||
|
||
t.Run("with search.folder filter", func(t *testing.T) {
|
||
fakeStore, fakeAIM, api := setupAPI(t)
|
||
|
||
// Create folders with different paths
|
||
folder1 := &folder.Folder{UID: "prod-uid", Title: "Production", Fullpath: "Production", OrgID: orgID}
|
||
folder2 := &folder.Folder{UID: "prod-alerts-uid", Title: "Alerts", Fullpath: "Production/Alerts", OrgID: orgID}
|
||
folder3 := &folder.Folder{UID: "dev-uid", Title: "Monitoring", Fullpath: "Development/Monitoring", OrgID: orgID}
|
||
folder4 := &folder.Folder{UID: "prod-crit-uid", Title: "Critical", Fullpath: "Production/Critical", OrgID: orgID}
|
||
fakeStore.Folders[orgID] = []*folder.Folder{folder1, folder2, folder3, folder4}
|
||
|
||
// Create rules in different folders
|
||
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
|
||
gen.WithNamespaceUID("prod-uid"), gen.WithUID("rule1"), gen.WithNoNotificationSettings())
|
||
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
|
||
gen.WithNamespaceUID("prod-alerts-uid"), gen.WithUID("rule2"), gen.WithNoNotificationSettings())
|
||
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
|
||
gen.WithNamespaceUID("dev-uid"), gen.WithUID("rule3"), gen.WithNoNotificationSettings())
|
||
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
|
||
gen.WithNamespaceUID("prod-crit-uid"), gen.WithUID("rule4"), gen.WithNoNotificationSettings())
|
||
|
||
testCases := []struct {
|
||
name string
|
||
searchFolder string
|
||
expectedUIDs []string
|
||
}{
|
||
{
|
||
name: "search 'production' matches Production folders",
|
||
searchFolder: "production",
|
||
expectedUIDs: []string{"rule1", "rule2", "rule4"},
|
||
},
|
||
{
|
||
name: "search 'prod alerts' matches Production/Alerts",
|
||
searchFolder: "prod alerts",
|
||
expectedUIDs: []string{"rule2"},
|
||
},
|
||
{
|
||
name: "search 'dev' matches Development",
|
||
searchFolder: "dev",
|
||
expectedUIDs: []string{"rule3"},
|
||
},
|
||
{
|
||
name: "search 'critical' matches Critical folder",
|
||
searchFolder: "critical",
|
||
expectedUIDs: []string{"rule4"},
|
||
},
|
||
{
|
||
name: "case insensitive search",
|
||
searchFolder: "PRODUCTION",
|
||
expectedUIDs: []string{"rule1", "rule2", "rule4"},
|
||
},
|
||
{
|
||
name: "empty search returns all rules",
|
||
searchFolder: "",
|
||
expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4"},
|
||
},
|
||
{
|
||
name: "non-matching search returns no rules",
|
||
searchFolder: "nonexistent",
|
||
expectedUIDs: []string{},
|
||
},
|
||
}
|
||
|
||
for _, tc := range testCases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
reqURL := "/api/v1/rules"
|
||
if tc.searchFolder != "" {
|
||
reqURL += "?search.folder=" + url.QueryEscape(tc.searchFolder)
|
||
}
|
||
req, err := http.NewRequest("GET", reqURL, nil)
|
||
require.NoError(t, err)
|
||
ctx := &contextmodel.ReqContext{
|
||
Context: &web.Context{Req: req},
|
||
SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions},
|
||
}
|
||
|
||
resp := api.RouteGetRuleStatuses(ctx)
|
||
require.Equal(t, http.StatusOK, resp.Status())
|
||
|
||
var res apimodels.RuleResponse
|
||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||
require.Equal(t, "success", res.Status)
|
||
|
||
// Collect rule UIDs from response
|
||
actualUIDs := []string{}
|
||
for _, group := range res.Data.RuleGroups {
|
||
for _, rule := range group.Rules {
|
||
actualUIDs = append(actualUIDs, rule.UID)
|
||
}
|
||
}
|
||
|
||
require.ElementsMatch(t, tc.expectedUIDs, actualUIDs)
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
func setupAPI(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv) {
|
||
fakeStore, fakeAIM, api, _ := setupAPIFull(t)
|
||
return fakeStore, fakeAIM, api
|
||
}
|
||
|
||
func setupAPIFull(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv, *fakes.FakeProvisioningStore) {
|
||
fakeStore := fakes.NewRuleStore(t)
|
||
fakeAIM := NewFakeAlertInstanceManager(t)
|
||
fakeSch := newFakeSchedulerReader(t).setupStates(fakeAIM)
|
||
fakeAuthz := &fakeRuleAccessControlService{}
|
||
fakeProvisioning := fakes.NewFakeProvisioningStore()
|
||
|
||
api := *NewPrometheusSrv(
|
||
log.NewNopLogger(),
|
||
fakeAIM,
|
||
fakeSch,
|
||
fakeStore,
|
||
fakeAuthz,
|
||
fakeProvisioning,
|
||
)
|
||
|
||
return fakeStore, fakeAIM, api, fakeProvisioning
|
||
}
|
||
|
||
func generateRuleAndInstanceWithQuery(t *testing.T, orgID int64, fakeAIM *fakeAlertInstanceManager, fakeStore *fakes.RuleStore, query ngmodels.AlertRuleMutator, additionalMutators ...ngmodels.AlertRuleMutator) {
|
||
t.Helper()
|
||
|
||
gen := ngmodels.RuleGen
|
||
r := gen.With(append([]ngmodels.AlertRuleMutator{gen.WithOrgID(orgID), asFixture(), query}, additionalMutators...)...).GenerateRef()
|
||
|
||
fakeAIM.GenerateAlertInstances(orgID, r.UID, 1, func(s *state.State) *state.State {
|
||
s.Labels = data.Labels{
|
||
"job": "prometheus",
|
||
alertingModels.NamespaceUIDLabel: "test_namespace_uid",
|
||
alertingModels.RuleUIDLabel: "test_alert_rule_uid_0",
|
||
}
|
||
s.Annotations = data.Labels{"severity": "critical"}
|
||
return s
|
||
})
|
||
|
||
fakeStore.PutRule(context.Background(), r)
|
||
}
|
||
|
||
// asFixture removes variable values of the alert rule.
|
||
// we're not too interested in variability of the rule in this scenario.
|
||
func asFixture() ngmodels.AlertRuleMutator {
|
||
return func(r *ngmodels.AlertRule) {
|
||
r.Title = "AlwaysFiring"
|
||
r.NamespaceUID = "namespaceUID"
|
||
r.RuleGroup = "rule-group"
|
||
r.UID = "RuleUID"
|
||
r.Labels = map[string]string{
|
||
"__a_private_label_on_the_rule__": "a_value",
|
||
alertingModels.RuleUIDLabel: "RuleUID",
|
||
}
|
||
r.Annotations = nil
|
||
r.IntervalSeconds = 60
|
||
r.For = 180 * time.Second
|
||
r.KeepFiringFor = 10 * time.Second
|
||
}
|
||
}
|
||
|
||
func withClassicConditionSingleQuery() ngmodels.AlertRuleMutator {
|
||
return func(r *ngmodels.AlertRule) {
|
||
queries := []ngmodels.AlertQuery{
|
||
{
|
||
RefID: "A",
|
||
QueryType: "",
|
||
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
||
DatasourceUID: "AUID",
|
||
Model: json.RawMessage(fmt.Sprintf(prometheusQueryModel, "A")),
|
||
},
|
||
{
|
||
RefID: "B",
|
||
QueryType: "",
|
||
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
||
DatasourceUID: expr.DatasourceUID,
|
||
Model: json.RawMessage(fmt.Sprintf(classicConditionsModel, "A", "B")),
|
||
},
|
||
}
|
||
r.Data = queries
|
||
}
|
||
}
|
||
|
||
func withExpressionsMultiQuery() ngmodels.AlertRuleMutator {
|
||
return func(r *ngmodels.AlertRule) {
|
||
queries := []ngmodels.AlertQuery{
|
||
{
|
||
RefID: "A",
|
||
QueryType: "",
|
||
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
||
DatasourceUID: "AUID",
|
||
Model: json.RawMessage(fmt.Sprintf(prometheusQueryModel, "A")),
|
||
},
|
||
{
|
||
RefID: "B",
|
||
QueryType: "",
|
||
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
||
DatasourceUID: "BUID",
|
||
Model: json.RawMessage(fmt.Sprintf(prometheusQueryModel, "B")),
|
||
},
|
||
{
|
||
RefID: "C",
|
||
QueryType: "",
|
||
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
||
DatasourceUID: expr.DatasourceUID,
|
||
Model: json.RawMessage(fmt.Sprintf(reduceLastExpressionModel, "A", "C")),
|
||
},
|
||
{
|
||
RefID: "D",
|
||
QueryType: "",
|
||
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
||
DatasourceUID: expr.DatasourceUID,
|
||
Model: json.RawMessage(fmt.Sprintf(reduceLastExpressionModel, "B", "D")),
|
||
},
|
||
{
|
||
RefID: "E",
|
||
QueryType: "",
|
||
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
||
DatasourceUID: expr.DatasourceUID,
|
||
Model: json.RawMessage(fmt.Sprintf(mathExpressionModel, "A", "B", "E")),
|
||
},
|
||
}
|
||
r.Data = queries
|
||
}
|
||
}
|