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
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 {

View File

@ -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")
}