| 
									
										
										
										
											2022-03-10 02:20:29 +08:00
										 |  |  | package api | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2022-03-14 18:39:20 +08:00
										 |  |  | 	"context" | 
					
						
							|  |  |  | 	"encoding/json" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2022-03-10 02:20:29 +08:00
										 |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"testing" | 
					
						
							| 
									
										
										
										
											2022-03-14 18:39:20 +08:00
										 |  |  | 	"time" | 
					
						
							| 
									
										
										
										
											2022-03-10 02:20:29 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/infra/log" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/models" | 
					
						
							| 
									
										
										
										
											2022-03-14 18:39:20 +08:00
										 |  |  | 	ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/services/ngalert/state" | 
					
						
							| 
									
										
										
										
											2022-03-10 02:20:29 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/ngalert/store" | 
					
						
							| 
									
										
										
										
											2022-03-14 18:39:20 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/util" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/web" | 
					
						
							| 
									
										
										
										
											2022-03-10 02:20:29 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-14 18:39:20 +08:00
										 |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/data" | 
					
						
							| 
									
										
										
										
											2022-03-10 02:20:29 +08:00
										 |  |  | 	"github.com/stretchr/testify/require" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func TestRouteGetAlertStatuses(t *testing.T) { | 
					
						
							|  |  |  | 	fakeStore := store.NewFakeRuleStore(t) | 
					
						
							|  |  |  | 	fakeAlertInstanceManager := NewFakeAlertInstanceManager(t) | 
					
						
							|  |  |  | 	orgID := int64(1) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-14 18:39:20 +08:00
										 |  |  | 	api := PrometheusSrv{ | 
					
						
							| 
									
										
										
										
											2022-03-10 02:20:29 +08:00
										 |  |  | 		log:     log.NewNopLogger(), | 
					
						
							|  |  |  | 		manager: fakeAlertInstanceManager, | 
					
						
							|  |  |  | 		store:   fakeStore, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	c := &models.ReqContext{SignedInUser: &models.SignedInUser{OrgId: orgID}} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("with no alerts", func(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2022-03-14 18:39:20 +08:00
										 |  |  | 		r := api.RouteGetAlertStatuses(c) | 
					
						
							| 
									
										
										
										
											2022-03-10 02:20:29 +08:00
										 |  |  | 		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) { | 
					
						
							| 
									
										
										
										
											2022-03-14 18:39:20 +08:00
										 |  |  | 		fakeAlertInstanceManager.GenerateAlertInstances(1, util.GenerateShortUID(), 2) | 
					
						
							|  |  |  | 		r := api.RouteGetAlertStatuses(c) | 
					
						
							| 
									
										
										
										
											2022-03-10 02:20:29 +08:00
										 |  |  | 		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())) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-03-14 18:39:20 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func TestRouteGetRuleStatuses(t *testing.T) { | 
					
						
							|  |  |  | 	timeNow = func() time.Time { return time.Date(2022, 3, 10, 14, 0, 0, 0, time.UTC) } | 
					
						
							|  |  |  | 	orgID := int64(1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	req, err := http.NewRequest("GET", "/api/v1/rules", nil) | 
					
						
							|  |  |  | 	require.NoError(t, err) | 
					
						
							|  |  |  | 	c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgId: orgID}} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	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()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		r := api.RouteGetRuleStatuses(c) | 
					
						
							|  |  |  | 		require.Equal(t, http.StatusOK, r.Status()) | 
					
						
							|  |  |  | 		require.JSONEq(t, ` | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	"status": "success", | 
					
						
							|  |  |  | 	"data": { | 
					
						
							|  |  |  | 		"groups": [{ | 
					
						
							|  |  |  | 			"name": "rule-group", | 
					
						
							|  |  |  | 			"file": "namespaceUID", | 
					
						
							|  |  |  | 			"rules": [{ | 
					
						
							|  |  |  | 				"state": "inactive", | 
					
						
							|  |  |  | 				"name": "AlwaysFiring", | 
					
						
							|  |  |  | 				"query": "vector(1)", | 
					
						
							|  |  |  | 				"alerts": [{ | 
					
						
							|  |  |  | 					"labels": { | 
					
						
							|  |  |  | 						"job": "prometheus" | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 					"annotations": { | 
					
						
							|  |  |  | 						"severity": "critical" | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 					"state": "Normal", | 
					
						
							|  |  |  | 					"activeAt": "0001-01-01T00:00:00Z", | 
					
						
							|  |  |  | 					"value": "" | 
					
						
							|  |  |  | 				}], | 
					
						
							|  |  |  | 				"labels": null, | 
					
						
							|  |  |  | 				"health": "ok", | 
					
						
							|  |  |  | 				"lastError": "", | 
					
						
							|  |  |  | 				"type": "alerting", | 
					
						
							|  |  |  | 				"lastEvaluation": "2022-03-10T14:01:00Z", | 
					
						
							|  |  |  | 				"duration": 180, | 
					
						
							|  |  |  | 				"evaluationTime": 60 | 
					
						
							|  |  |  | 			}], | 
					
						
							|  |  |  | 			"interval": 60, | 
					
						
							|  |  |  | 			"lastEvaluation": "2022-03-10T14:01:00Z", | 
					
						
							|  |  |  | 			"evaluationTime": 0 | 
					
						
							|  |  |  | 		}] | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | `, 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()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		r := api.RouteGetRuleStatuses(c) | 
					
						
							|  |  |  | 		require.Equal(t, http.StatusOK, r.Status()) | 
					
						
							|  |  |  | 		require.JSONEq(t, ` | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	"status": "success", | 
					
						
							|  |  |  | 	"data": { | 
					
						
							|  |  |  | 		"groups": [{ | 
					
						
							|  |  |  | 			"name": "rule-group", | 
					
						
							|  |  |  | 			"file": "namespaceUID", | 
					
						
							|  |  |  | 			"rules": [{ | 
					
						
							|  |  |  | 				"state": "inactive", | 
					
						
							|  |  |  | 				"name": "AlwaysFiring", | 
					
						
							|  |  |  | 				"query": "vector(1) | vector(1)", | 
					
						
							|  |  |  | 				"alerts": [{ | 
					
						
							|  |  |  | 					"labels": { | 
					
						
							|  |  |  | 						"job": "prometheus" | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 					"annotations": { | 
					
						
							|  |  |  | 						"severity": "critical" | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 					"state": "Normal", | 
					
						
							|  |  |  | 					"activeAt": "0001-01-01T00:00:00Z", | 
					
						
							|  |  |  | 					"value": "" | 
					
						
							|  |  |  | 				}], | 
					
						
							|  |  |  | 				"labels": null, | 
					
						
							|  |  |  | 				"health": "ok", | 
					
						
							|  |  |  | 				"lastError": "", | 
					
						
							|  |  |  | 				"type": "alerting", | 
					
						
							|  |  |  | 				"lastEvaluation": "2022-03-10T14:01:00Z", | 
					
						
							|  |  |  | 				"duration": 180, | 
					
						
							|  |  |  | 				"evaluationTime": 60 | 
					
						
							|  |  |  | 			}], | 
					
						
							|  |  |  | 			"interval": 60, | 
					
						
							|  |  |  | 			"lastEvaluation": "2022-03-10T14:01:00Z", | 
					
						
							|  |  |  | 			"evaluationTime": 0 | 
					
						
							|  |  |  | 		}] | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | `, string(r.Body())) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func setupAPI(t *testing.T) (*store.FakeRuleStore, *fakeAlertInstanceManager, PrometheusSrv) { | 
					
						
							|  |  |  | 	fakeStore := store.NewFakeRuleStore(t) | 
					
						
							|  |  |  | 	fakeAIM := NewFakeAlertInstanceManager(t) | 
					
						
							|  |  |  | 	api := PrometheusSrv{ | 
					
						
							|  |  |  | 		log:     log.NewNopLogger(), | 
					
						
							|  |  |  | 		manager: fakeAIM, | 
					
						
							|  |  |  | 		store:   fakeStore, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return fakeStore, fakeAIM, api | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func generateRuleAndInstanceWithQuery(t *testing.T, orgID int64, fakeAIM *fakeAlertInstanceManager, fakeStore *store.FakeRuleStore, query func(r *ngmodels.AlertRule)) { | 
					
						
							|  |  |  | 	t.Helper() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	rules := ngmodels.GenerateAlertRules(1, ngmodels.AlertRuleGen(withOrgID(orgID), asFixture(), query)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1, func(s *state.State) *state.State { | 
					
						
							|  |  |  | 		s.Labels = data.Labels{"job": "prometheus"} | 
					
						
							|  |  |  | 		s.Annotations = data.Labels{"severity": "critical"} | 
					
						
							|  |  |  | 		return s | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, r := range rules { | 
					
						
							|  |  |  | 		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() func(r *ngmodels.AlertRule) { | 
					
						
							|  |  |  | 	return func(r *ngmodels.AlertRule) { | 
					
						
							|  |  |  | 		r.Title = "AlwaysFiring" | 
					
						
							|  |  |  | 		r.NamespaceUID = "namespaceUID" | 
					
						
							|  |  |  | 		r.RuleGroup = "rule-group" | 
					
						
							|  |  |  | 		r.UID = "RuleUID" | 
					
						
							|  |  |  | 		r.Labels = nil | 
					
						
							|  |  |  | 		r.Annotations = nil | 
					
						
							|  |  |  | 		r.IntervalSeconds = 60 | 
					
						
							|  |  |  | 		r.For = 180 * time.Second | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func withClassicConditionSingleQuery() func(r *ngmodels.AlertRule) { | 
					
						
							|  |  |  | 	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:     "-100", | 
					
						
							|  |  |  | 				Model:             json.RawMessage(fmt.Sprintf(classicConditionsModel, "A", "B")), | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		r.Data = queries | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func withExpressionsMultiQuery() func(r *ngmodels.AlertRule) { | 
					
						
							|  |  |  | 	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:     "-100", | 
					
						
							|  |  |  | 				Model:             json.RawMessage(fmt.Sprintf(reduceLastExpressionModel, "A", "C")), | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				RefID:             "D", | 
					
						
							|  |  |  | 				QueryType:         "", | 
					
						
							|  |  |  | 				RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)}, | 
					
						
							|  |  |  | 				DatasourceUID:     "-100", | 
					
						
							|  |  |  | 				Model:             json.RawMessage(fmt.Sprintf(reduceLastExpressionModel, "B", "D")), | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				RefID:             "E", | 
					
						
							|  |  |  | 				QueryType:         "", | 
					
						
							|  |  |  | 				RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)}, | 
					
						
							|  |  |  | 				DatasourceUID:     "-100", | 
					
						
							|  |  |  | 				Model:             json.RawMessage(fmt.Sprintf(mathExpressionModel, "A", "B", "E")), | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		r.Data = queries | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } |