mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Refactor api_prometheus.go request handlers. (#86639)
This splits the request handlers into two functions, one which is the actual handler and one which is independent from the Grafana `ReqContext` object. This is to make it easier to reuse the implementation in other code. Part of the refactoring changes the functions which get query parameters from the request to operate on a `url.Values` instead of the request object. The change also makes the code consistently use `req.Form` instead of a combination of `req.URL.Query()` and `req.Form`, though I have left `api_ruler` as-is to avoid this PR growing too large.
This commit is contained in:
parent
68564b1940
commit
a6ad2380bf
@ -1,10 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
@ -17,7 +17,6 @@ import (
|
||||
"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/folder"
|
||||
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"
|
||||
@ -61,6 +60,20 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo
|
||||
// As we are using req.Form directly, this triggers a call to ParseForm() if needed.
|
||||
c.Query("")
|
||||
|
||||
resp := PrepareAlertStatuses(srv.manager, AlertStatusesOptions{
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
Query: c.Req.Form,
|
||||
})
|
||||
|
||||
return response.JSON(resp.HTTPStatusCode(), resp)
|
||||
}
|
||||
|
||||
type AlertStatusesOptions struct {
|
||||
OrgID int64
|
||||
Query url.Values
|
||||
}
|
||||
|
||||
func PrepareAlertStatuses(manager state.AlertInstanceManager, opts AlertStatusesOptions) apimodels.AlertResponse {
|
||||
alertResponse := apimodels.AlertResponse{
|
||||
DiscoveryBase: apimodels.DiscoveryBase{
|
||||
Status: "success",
|
||||
@ -71,11 +84,11 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo
|
||||
}
|
||||
|
||||
var labelOptions []ngmodels.LabelOption
|
||||
if !getBoolWithDefault(c.Req.Form, queryIncludeInternalLabels, false) {
|
||||
if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) {
|
||||
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
|
||||
}
|
||||
|
||||
for _, alertState := range srv.manager.GetAll(c.SignedInUser.GetOrgID()) {
|
||||
for _, alertState := range manager.GetAll(opts.OrgID) {
|
||||
startsAt := alertState.StartsAt
|
||||
valString := ""
|
||||
|
||||
@ -95,7 +108,7 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo
|
||||
})
|
||||
}
|
||||
|
||||
return response.JSON(alertResponse.HTTPStatusCode(), alertResponse)
|
||||
return alertResponse
|
||||
}
|
||||
|
||||
func formatValues(alertState *state.State) string {
|
||||
@ -126,16 +139,16 @@ func formatValues(alertState *state.State) string {
|
||||
return fv
|
||||
}
|
||||
|
||||
func getPanelIDFromRequest(r *http.Request) (int64, error) {
|
||||
if s := strings.TrimSpace(r.URL.Query().Get("panel_id")); s != "" {
|
||||
func getPanelIDFromQuery(v url.Values) (int64, error) {
|
||||
if s := strings.TrimSpace(v.Get("panel_id")); s != "" {
|
||||
return strconv.ParseInt(s, 10, 64)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func getMatchersFromRequest(r *http.Request) (labels.Matchers, error) {
|
||||
func getMatchersFromQuery(v url.Values) (labels.Matchers, error) {
|
||||
var matchers labels.Matchers
|
||||
for _, s := range r.URL.Query()["matcher"] {
|
||||
for _, s := range v["matcher"] {
|
||||
var m labels.Matcher
|
||||
if err := json.Unmarshal([]byte(s), &m); err != nil {
|
||||
return nil, err
|
||||
@ -148,9 +161,9 @@ func getMatchersFromRequest(r *http.Request) (labels.Matchers, error) {
|
||||
return matchers, nil
|
||||
}
|
||||
|
||||
func getStatesFromRequest(r *http.Request) ([]eval.State, error) {
|
||||
func getStatesFromQuery(v url.Values) ([]eval.State, error) {
|
||||
var states []eval.State
|
||||
for _, s := range r.URL.Query()["state"] {
|
||||
for _, s := range v["state"] {
|
||||
s = strings.ToLower(s)
|
||||
switch s {
|
||||
case "normal", "inactive":
|
||||
@ -171,6 +184,18 @@ func getStatesFromRequest(r *http.Request) ([]eval.State, error) {
|
||||
return states, nil
|
||||
}
|
||||
|
||||
type RuleGroupStatusesOptions struct {
|
||||
Ctx context.Context
|
||||
OrgID int64
|
||||
Query url.Values
|
||||
Namespaces map[string]string
|
||||
AuthorizeRuleGroup func(rules []*ngmodels.AlertRule) (bool, error)
|
||||
}
|
||||
|
||||
type ListAlertRulesStore interface {
|
||||
ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) (ngmodels.RulesGroup, error)
|
||||
}
|
||||
|
||||
func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) response.Response {
|
||||
// As we are using req.Form directly, this triggers a call to ParseForm() if needed.
|
||||
c.Query("")
|
||||
@ -184,48 +209,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
|
||||
},
|
||||
}
|
||||
|
||||
dashboardUID := c.Query("dashboard_uid")
|
||||
panelID, err := getPanelIDFromRequest(c.Req)
|
||||
if err != nil {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = fmt.Sprintf("invalid panel_id: %s", err.Error())
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
}
|
||||
if dashboardUID == "" && panelID != 0 {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = "panel_id must be set with dashboard_uid"
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
}
|
||||
|
||||
limitGroups := getInt64WithDefault(c.Req.Form, "limit", -1)
|
||||
limitRulesPerGroup := getInt64WithDefault(c.Req.Form, "limit_rules", -1)
|
||||
limitAlertsPerRule := getInt64WithDefault(c.Req.Form, "limit_alerts", -1)
|
||||
matchers, err := getMatchersFromRequest(c.Req)
|
||||
if err != nil {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = err.Error()
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
}
|
||||
withStates, err := getStatesFromRequest(c.Req)
|
||||
if err != nil {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = err.Error()
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
}
|
||||
withStatesFast := make(map[eval.State]struct{})
|
||||
for _, state := range withStates {
|
||||
withStatesFast[state] = struct{}{}
|
||||
}
|
||||
|
||||
var labelOptions []ngmodels.LabelOption
|
||||
if !getBoolWithDefault(c.Req.Form, queryIncludeInternalLabels, false) {
|
||||
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
|
||||
}
|
||||
|
||||
namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.SignedInUser.GetOrgID(), c.SignedInUser)
|
||||
if err != nil {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
@ -234,28 +217,98 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
}
|
||||
|
||||
if len(namespaceMap) == 0 {
|
||||
srv.log.Debug("User does not have access to any namespaces")
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
namespaces := map[string]string{}
|
||||
for namespaceUID, folder := range namespaceMap {
|
||||
namespaces[namespaceUID] = folder.Fullpath
|
||||
}
|
||||
|
||||
namespaceUIDs := make([]string, len(namespaceMap))
|
||||
for k := range namespaceMap {
|
||||
ruleResponse = PrepareRuleGroupStatuses(srv.log, srv.manager, srv.store, RuleGroupStatusesOptions{
|
||||
Ctx: c.Req.Context(),
|
||||
OrgID: c.OrgID,
|
||||
Query: c.Req.Form,
|
||||
Namespaces: namespaces,
|
||||
AuthorizeRuleGroup: func(rules []*ngmodels.AlertRule) (bool, error) {
|
||||
return srv.authz.HasAccessToRuleGroup(c.Req.Context(), c.SignedInUser, rules)
|
||||
},
|
||||
})
|
||||
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
}
|
||||
|
||||
func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse {
|
||||
ruleResponse := apimodels.RuleResponse{
|
||||
DiscoveryBase: apimodels.DiscoveryBase{
|
||||
Status: "success",
|
||||
},
|
||||
Data: apimodels.RuleDiscovery{
|
||||
RuleGroups: []apimodels.RuleGroup{},
|
||||
},
|
||||
}
|
||||
|
||||
dashboardUID := opts.Query.Get("dashboard_uid")
|
||||
panelID, err := getPanelIDFromQuery(opts.Query)
|
||||
if err != nil {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = fmt.Sprintf("invalid panel_id: %s", err.Error())
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData
|
||||
return ruleResponse
|
||||
}
|
||||
if dashboardUID == "" && panelID != 0 {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = "panel_id must be set with dashboard_uid"
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData
|
||||
return ruleResponse
|
||||
}
|
||||
|
||||
limitGroups := getInt64WithDefault(opts.Query, "limit", -1)
|
||||
limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1)
|
||||
limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1)
|
||||
matchers, err := getMatchersFromQuery(opts.Query)
|
||||
if err != nil {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = err.Error()
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData
|
||||
return ruleResponse
|
||||
}
|
||||
withStates, err := getStatesFromQuery(opts.Query)
|
||||
if err != nil {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = err.Error()
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData
|
||||
return ruleResponse
|
||||
}
|
||||
withStatesFast := make(map[eval.State]struct{})
|
||||
for _, state := range withStates {
|
||||
withStatesFast[state] = struct{}{}
|
||||
}
|
||||
|
||||
var labelOptions []ngmodels.LabelOption
|
||||
if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) {
|
||||
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
|
||||
}
|
||||
|
||||
if len(opts.Namespaces) == 0 {
|
||||
log.Debug("User does not have access to any namespaces")
|
||||
return ruleResponse
|
||||
}
|
||||
|
||||
namespaceUIDs := make([]string, len(opts.Namespaces))
|
||||
for k := range opts.Namespaces {
|
||||
namespaceUIDs = append(namespaceUIDs, k)
|
||||
}
|
||||
|
||||
alertRuleQuery := ngmodels.ListAlertRulesQuery{
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
OrgID: opts.OrgID,
|
||||
NamespaceUIDs: namespaceUIDs,
|
||||
DashboardUID: dashboardUID,
|
||||
PanelID: panelID,
|
||||
}
|
||||
ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &alertRuleQuery)
|
||||
ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery)
|
||||
if err != nil {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = fmt.Sprintf("failure getting rules: %s", err.Error())
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
return ruleResponse
|
||||
}
|
||||
|
||||
// Group rules together by Namespace and Rule Group. Rules are also grouped by Org ID,
|
||||
@ -275,22 +328,22 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
|
||||
|
||||
rulesTotals := make(map[string]int64, len(groupedRules))
|
||||
for groupKey, rules := range groupedRules {
|
||||
folder := namespaceMap[groupKey.NamespaceUID]
|
||||
if folder == nil {
|
||||
srv.log.Warn("Query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID)
|
||||
folder, ok := opts.Namespaces[groupKey.NamespaceUID]
|
||||
if !ok {
|
||||
log.Warn("Query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID)
|
||||
continue
|
||||
}
|
||||
ok, err := srv.authz.HasAccessToRuleGroup(c.Req.Context(), c.SignedInUser, rules)
|
||||
ok, err := opts.AuthorizeRuleGroup(rules)
|
||||
if err != nil {
|
||||
ruleResponse.DiscoveryBase.Status = "error"
|
||||
ruleResponse.DiscoveryBase.Error = fmt.Sprintf("cannot authorize access to rule group: %s", err.Error())
|
||||
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
return ruleResponse
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ruleGroup, totals := srv.toRuleGroup(groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions)
|
||||
ruleGroup, totals := toRuleGroup(log, manager, groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions)
|
||||
ruleGroup.Totals = totals
|
||||
for k, v := range totals {
|
||||
rulesTotals[k] += v
|
||||
@ -335,7 +388,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
|
||||
ruleResponse.Data.RuleGroups = ruleResponse.Data.RuleGroups[0:limitGroups]
|
||||
}
|
||||
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
return ruleResponse
|
||||
}
|
||||
|
||||
// This is the same as matchers.Matches but avoids the need to create a LabelSet
|
||||
@ -348,11 +401,11 @@ func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder *folder.Folder, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) {
|
||||
func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) {
|
||||
newGroup := &apimodels.RuleGroup{
|
||||
Name: groupKey.RuleGroup,
|
||||
// file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana.
|
||||
File: folder.Fullpath,
|
||||
File: folderFullPath,
|
||||
}
|
||||
|
||||
rulesTotals := make(map[string]int64, len(rules))
|
||||
@ -362,7 +415,7 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder
|
||||
alertingRule := apimodels.AlertingRule{
|
||||
State: "inactive",
|
||||
Name: rule.Title,
|
||||
Query: ruleToQuery(srv.log, rule),
|
||||
Query: ruleToQuery(log, rule),
|
||||
Duration: rule.For.Seconds(),
|
||||
Annotations: rule.Annotations,
|
||||
}
|
||||
@ -375,7 +428,7 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder
|
||||
LastEvaluation: time.Time{},
|
||||
}
|
||||
|
||||
states := srv.manager.GetStatesForRuleUID(rule.OrgID, rule.UID)
|
||||
states := manager.GetStatesForRuleUID(rule.OrgID, rule.UID)
|
||||
totals := make(map[string]int64)
|
||||
totalsFiltered := make(map[string]int64)
|
||||
for _, alertState := range states {
|
||||
|
@ -234,7 +234,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res
|
||||
}
|
||||
|
||||
dashboardUID := c.Query("dashboard_uid")
|
||||
panelID, err := getPanelIDFromRequest(c.Req)
|
||||
panelID, err := getPanelIDFromQuery(c.Req.URL.Query())
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusBadRequest, err, "invalid panel_id")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user