mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			2169 lines
		
	
	
		
			74 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			2169 lines
		
	
	
		
			74 KiB
		
	
	
	
		
			Go
		
	
	
	
| 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
 | |
| 	}
 | |
| }
 |