The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/api/api_prometheus_test.go

2168 lines
74 KiB

package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"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/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
}
}
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("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 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")
})
}
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
}
}