diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 045fc3e0e06..1655416a28c 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -84,6 +84,7 @@ type API struct { AlertsRouter *sender.AlertsRouter EvaluatorFactory eval.EvaluatorFactory FeatureManager featuremgmt.FeatureToggles + Historian Historian AppUrl *url.URL } @@ -152,6 +153,11 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { muteTimings: api.MuteTimings, alertRules: api.AlertRules, }), m) + + api.RegisterHistoryApiEndpoints(NewStateHistoryApi(&HistorySrv{ + logger: logger, + hist: api.Historian, + }), m) } func (api *API) Usage(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) { diff --git a/pkg/services/ngalert/api/api_ruler_history.go b/pkg/services/ngalert/api/api_ruler_history.go new file mode 100644 index 00000000000..1d3d78be745 --- /dev/null +++ b/pkg/services/ngalert/api/api_ruler_history.go @@ -0,0 +1,40 @@ +package api + +import ( + "context" + "net/http" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/infra/log" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +type Historian interface { + QueryStates(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) +} + +type HistorySrv struct { + logger log.Logger + hist Historian +} + +func (srv *HistorySrv) RouteQueryStateHistory(c *contextmodel.ReqContext) response.Response { + from := c.QueryInt64("from") + to := c.QueryInt64("to") + query := models.HistoryQuery{ + RuleUID: c.Query("ruleUID"), + OrgID: c.OrgID, + SignedInUser: c.SignedInUser, + From: time.Unix(from, 0), + To: time.Unix(to, 0), + Labels: map[string]string{}, // TODO, not supported by all backends yet. + } + frame, err := srv.hist.QueryStates(c.Req.Context(), query) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, frame) +} diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 8422ad66872..2a9ffbcb159 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -58,6 +58,10 @@ func (api *API) authorize(method, path string) web.Handler { ac.EvalPermission(ac.ActionAlertingRuleCreate, scope), ac.EvalPermission(ac.ActionAlertingRuleDelete, scope), ) + // Grafana rule state history paths + case http.MethodGet + "/api/v1/rules/history": + fallback = middleware.ReqSignedIn + eval = ac.EvalPermission(ac.ActionAlertingRuleRead) // Grafana, Prometheus-compatible Paths case http.MethodGet + "/api/prometheus/grafana/api/v1/rules": diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index 1daba37d64e..7fa81720d3d 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -49,7 +49,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 44) + require.Len(t, paths, 45) ac := acmock.New() api := &API{AccessControl: ac} diff --git a/pkg/services/ngalert/api/generated_base_api_history.go b/pkg/services/ngalert/api/generated_base_api_history.go new file mode 100644 index 00000000000..8f352d366c8 --- /dev/null +++ b/pkg/services/ngalert/api/generated_base_api_history.go @@ -0,0 +1,40 @@ +/*Package api contains base API implementation of unified alerting + * + *Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + * + *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. + */ +package api + +import ( + "net/http" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/middleware" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/ngalert/metrics" +) + +type HistoryApi interface { + RouteGetStateHistory(*contextmodel.ReqContext) response.Response +} + +func (f *HistoryApiHandler) RouteGetStateHistory(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteGetStateHistory(ctx) +} + +func (api *API) RegisterHistoryApiEndpoints(srv HistoryApi, m *metrics.API) { + api.RouteRegister.Group("", func(group routing.RouteRegister) { + group.Get( + toMacaronPath("/api/v1/rules/history"), + api.authorize(http.MethodGet, "/api/v1/rules/history"), + metrics.Instrument( + http.MethodGet, + "/api/v1/rules/history", + srv.RouteGetStateHistory, + m, + ), + ) + }, middleware.ReqSignedIn) +} diff --git a/pkg/services/ngalert/api/ruler_history.go b/pkg/services/ngalert/api/ruler_history.go new file mode 100644 index 00000000000..3e3b39277cc --- /dev/null +++ b/pkg/services/ngalert/api/ruler_history.go @@ -0,0 +1,20 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/api/response" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" +) + +type HistoryApiHandler struct { + svc *HistorySrv +} + +func NewStateHistoryApi(svc *HistorySrv) *HistoryApiHandler { + return &HistoryApiHandler{ + svc: svc, + } +} + +func (f *HistoryApiHandler) handleRouteGetStateHistory(ctx *contextmodel.ReqContext) response.Response { + return f.svc.RouteQueryStateHistory(ctx) +} diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 5efd5113f48..779343042be 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -3931,7 +3931,6 @@ "type": "object" }, "receiver": { - "description": "Receiver receiver", "properties": { "active": { "description": "active", @@ -4885,4 +4884,4 @@ } }, "swagger": "2.0" -} +} \ No newline at end of file diff --git a/pkg/services/ngalert/api/tooling/definitions/ruler_state_history.go b/pkg/services/ngalert/api/tooling/definitions/ruler_state_history.go new file mode 100644 index 00000000000..4c31b416cf3 --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/ruler_state_history.go @@ -0,0 +1,17 @@ +package definitions + +import "github.com/grafana/grafana-plugin-sdk-go/data" + +// swagger:route GET /api/v1/rules/history history RouteGetStateHistory +// +// Query state history. +// +// Produces: +// - application/json +// +// Responses: +// 200: StateHistory + +type StateHistory struct { + Results *data.Frame `json:"results"` +} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 5bfb8c385e1..8b9565b87c9 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -6642,6 +6642,23 @@ "testing" ] } + }, + "/api/v1/rules/history": { + "get": { + "operationId": "RouteGetStateHistory", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "$ref": "#/responses/StateHistory" + } + }, + "summary": "Query state history.", + "tags": [ + "history" + ] + } } }, "produces": [ diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 1e3eb9070b4..5f16d5c6943 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -2632,6 +2632,23 @@ } } } + }, + "/api/v1/rules/history": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "history" + ], + "summary": "Query state history.", + "operationId": "RouteGetStateHistory", + "responses": { + "200": { + "$ref": "#/responses/StateHistory" + } + } + } } }, "definitions": { @@ -6330,6 +6347,7 @@ "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" @@ -6708,4 +6726,4 @@ "type": "basic" } } -} +} \ No newline at end of file diff --git a/pkg/services/ngalert/models/history.go b/pkg/services/ngalert/models/history.go index c49ebd8a9ce..ed390d8f736 100644 --- a/pkg/services/ngalert/models/history.go +++ b/pkg/services/ngalert/models/history.go @@ -1,12 +1,17 @@ package models -import "time" +import ( + "time" + + "github.com/grafana/grafana/pkg/services/user" +) // HistoryQuery represents a query for alert state history. type HistoryQuery struct { - RuleUID string - OrgID int64 - Labels map[string]string - From time.Time - To time.Time + RuleUID string + OrgID int64 + Labels map[string]string + From time.Time + To time.Time + SignedInUser *user.SignedInUser } diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 232ef0ac3f5..dd7e93ac4f4 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -268,6 +268,7 @@ func (ng *AlertNG) init() error { EvaluatorFactory: evalFactory, FeatureManager: ng.FeatureToggles, AppUrl: appUrl, + Historian: history, } api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) @@ -383,7 +384,12 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { return limits, nil } -func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingStateHistorySettings, ar annotations.Repository, ds dashboards.DashboardService, rs historian.RuleStore) (state.Historian, error) { +type Historian interface { + api.Historian + state.Historian +} + +func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingStateHistorySettings, ar annotations.Repository, ds dashboards.DashboardService, rs historian.RuleStore) (Historian, error) { if !cfg.Enabled { return historian.NewNopHistorian(), nil } diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index 7d28512ea23..d354e55d82c 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -79,10 +79,11 @@ func (h *AnnotationBackend) QueryStates(ctx context.Context, query ngmodels.Hist } q := annotations.ItemQuery{ - AlertId: rq.Result.ID, - OrgId: query.OrgID, - From: query.From.Unix(), - To: query.To.Unix(), + AlertId: rq.Result.ID, + OrgId: query.OrgID, + From: query.From.Unix(), + To: query.To.Unix(), + SignedInUser: query.SignedInUser, } items, err := h.annotations.Find(ctx, &q) if err != nil { diff --git a/pkg/services/ngalert/state/historian/noop.go b/pkg/services/ngalert/state/historian/noop.go index 2acee1ace1f..408f559faeb 100644 --- a/pkg/services/ngalert/state/historian/noop.go +++ b/pkg/services/ngalert/state/historian/noop.go @@ -3,6 +3,8 @@ package historian import ( "context" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) @@ -19,3 +21,7 @@ func (f *NoOpHistorian) RecordStatesAsync(ctx context.Context, _ history_model.R close(errCh) return errCh } + +func (f *NoOpHistorian) QueryStates(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + return data.NewFrame("states"), nil +} diff --git a/public/api-merged.json b/public/api-merged.json index cfbcf97a511..405e76fca9d 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -19076,6 +19076,7 @@ } }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert"