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:
Steve Simpson 2024-04-23 14:50:26 +02:00 committed by GitHub
parent 68564b1940
commit a6ad2380bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 126 additions and 73 deletions

View File

@ -1,10 +1,10 @@
package api package api
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"sort" "sort"
"strconv" "strconv"
@ -17,7 +17,6 @@ import (
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" 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" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/eval"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" 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. // As we are using req.Form directly, this triggers a call to ParseForm() if needed.
c.Query("") 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{ alertResponse := apimodels.AlertResponse{
DiscoveryBase: apimodels.DiscoveryBase{ DiscoveryBase: apimodels.DiscoveryBase{
Status: "success", Status: "success",
@ -71,11 +84,11 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo
} }
var labelOptions []ngmodels.LabelOption var labelOptions []ngmodels.LabelOption
if !getBoolWithDefault(c.Req.Form, queryIncludeInternalLabels, false) { if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) {
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
} }
for _, alertState := range srv.manager.GetAll(c.SignedInUser.GetOrgID()) { for _, alertState := range manager.GetAll(opts.OrgID) {
startsAt := alertState.StartsAt startsAt := alertState.StartsAt
valString := "" 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 { func formatValues(alertState *state.State) string {
@ -126,16 +139,16 @@ func formatValues(alertState *state.State) string {
return fv return fv
} }
func getPanelIDFromRequest(r *http.Request) (int64, error) { func getPanelIDFromQuery(v url.Values) (int64, error) {
if s := strings.TrimSpace(r.URL.Query().Get("panel_id")); s != "" { if s := strings.TrimSpace(v.Get("panel_id")); s != "" {
return strconv.ParseInt(s, 10, 64) return strconv.ParseInt(s, 10, 64)
} }
return 0, nil return 0, nil
} }
func getMatchersFromRequest(r *http.Request) (labels.Matchers, error) { func getMatchersFromQuery(v url.Values) (labels.Matchers, error) {
var matchers labels.Matchers var matchers labels.Matchers
for _, s := range r.URL.Query()["matcher"] { for _, s := range v["matcher"] {
var m labels.Matcher var m labels.Matcher
if err := json.Unmarshal([]byte(s), &m); err != nil { if err := json.Unmarshal([]byte(s), &m); err != nil {
return nil, err return nil, err
@ -148,9 +161,9 @@ func getMatchersFromRequest(r *http.Request) (labels.Matchers, error) {
return matchers, nil return matchers, nil
} }
func getStatesFromRequest(r *http.Request) ([]eval.State, error) { func getStatesFromQuery(v url.Values) ([]eval.State, error) {
var states []eval.State var states []eval.State
for _, s := range r.URL.Query()["state"] { for _, s := range v["state"] {
s = strings.ToLower(s) s = strings.ToLower(s)
switch s { switch s {
case "normal", "inactive": case "normal", "inactive":
@ -171,6 +184,18 @@ func getStatesFromRequest(r *http.Request) ([]eval.State, error) {
return states, nil 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 { func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) response.Response {
// As we are using req.Form directly, this triggers a call to ParseForm() if needed. // As we are using req.Form directly, this triggers a call to ParseForm() if needed.
c.Query("") 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) namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil { if err != nil {
ruleResponse.DiscoveryBase.Status = "error" ruleResponse.DiscoveryBase.Status = "error"
@ -234,28 +217,98 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
} }
if len(namespaceMap) == 0 { namespaces := map[string]string{}
srv.log.Debug("User does not have access to any namespaces") for namespaceUID, folder := range namespaceMap {
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) namespaces[namespaceUID] = folder.Fullpath
} }
namespaceUIDs := make([]string, len(namespaceMap)) ruleResponse = PrepareRuleGroupStatuses(srv.log, srv.manager, srv.store, RuleGroupStatusesOptions{
for k := range namespaceMap { 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) namespaceUIDs = append(namespaceUIDs, k)
} }
alertRuleQuery := ngmodels.ListAlertRulesQuery{ alertRuleQuery := ngmodels.ListAlertRulesQuery{
OrgID: c.SignedInUser.GetOrgID(), OrgID: opts.OrgID,
NamespaceUIDs: namespaceUIDs, NamespaceUIDs: namespaceUIDs,
DashboardUID: dashboardUID, DashboardUID: dashboardUID,
PanelID: panelID, PanelID: panelID,
} }
ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &alertRuleQuery) ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery)
if err != nil { if err != nil {
ruleResponse.DiscoveryBase.Status = "error" ruleResponse.DiscoveryBase.Status = "error"
ruleResponse.DiscoveryBase.Error = fmt.Sprintf("failure getting rules: %s", err.Error()) ruleResponse.DiscoveryBase.Error = fmt.Sprintf("failure getting rules: %s", err.Error())
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer 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, // 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)) rulesTotals := make(map[string]int64, len(groupedRules))
for groupKey, rules := range groupedRules { for groupKey, rules := range groupedRules {
folder := namespaceMap[groupKey.NamespaceUID] folder, ok := opts.Namespaces[groupKey.NamespaceUID]
if folder == nil { if !ok {
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) 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 continue
} }
ok, err := srv.authz.HasAccessToRuleGroup(c.Req.Context(), c.SignedInUser, rules) ok, err := opts.AuthorizeRuleGroup(rules)
if err != nil { if err != nil {
ruleResponse.DiscoveryBase.Status = "error" ruleResponse.DiscoveryBase.Status = "error"
ruleResponse.DiscoveryBase.Error = fmt.Sprintf("cannot authorize access to rule group: %s", err.Error()) ruleResponse.DiscoveryBase.Error = fmt.Sprintf("cannot authorize access to rule group: %s", err.Error())
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) return ruleResponse
} }
if !ok { if !ok {
continue 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 ruleGroup.Totals = totals
for k, v := range totals { for k, v := range totals {
rulesTotals[k] += v rulesTotals[k] += v
@ -335,7 +388,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
ruleResponse.Data.RuleGroups = ruleResponse.Data.RuleGroups[0:limitGroups] 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 // 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 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{ newGroup := &apimodels.RuleGroup{
Name: groupKey.RuleGroup, Name: groupKey.RuleGroup,
// file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana. // 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)) rulesTotals := make(map[string]int64, len(rules))
@ -362,7 +415,7 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder
alertingRule := apimodels.AlertingRule{ alertingRule := apimodels.AlertingRule{
State: "inactive", State: "inactive",
Name: rule.Title, Name: rule.Title,
Query: ruleToQuery(srv.log, rule), Query: ruleToQuery(log, rule),
Duration: rule.For.Seconds(), Duration: rule.For.Seconds(),
Annotations: rule.Annotations, Annotations: rule.Annotations,
} }
@ -375,7 +428,7 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder
LastEvaluation: time.Time{}, LastEvaluation: time.Time{},
} }
states := srv.manager.GetStatesForRuleUID(rule.OrgID, rule.UID) states := manager.GetStatesForRuleUID(rule.OrgID, rule.UID)
totals := make(map[string]int64) totals := make(map[string]int64)
totalsFiltered := make(map[string]int64) totalsFiltered := make(map[string]int64)
for _, alertState := range states { for _, alertState := range states {

View File

@ -234,7 +234,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res
} }
dashboardUID := c.Query("dashboard_uid") dashboardUID := c.Query("dashboard_uid")
panelID, err := getPanelIDFromRequest(c.Req) panelID, err := getPanelIDFromQuery(c.Req.URL.Query())
if err != nil { if err != nil {
return ErrResp(http.StatusBadRequest, err, "invalid panel_id") return ErrResp(http.StatusBadRequest, err, "invalid panel_id")
} }