From 2179a2658e88c97e311c9c1c8e37f7eca152b8e9 Mon Sep 17 00:00:00 2001 From: Owen Diehl Date: Wed, 24 Mar 2021 07:43:25 -0400 Subject: [PATCH] Extricates reusable utilities for different alerting proxy types (#32268) * backendtype helper * abstracts alertingproxy * updates alerting api dep * prom endpoints --- go.mod | 2 +- go.sum | 8 +- pkg/services/ngalert/api/api.go | 11 ++- pkg/services/ngalert/api/fork_ruler.go | 31 ++----- pkg/services/ngalert/api/forked_prom.go | 55 ++++++++++++ pkg/services/ngalert/api/lotex.go | 84 ++---------------- pkg/services/ngalert/api/lotex_prom.go | 51 +++++++++++ pkg/services/ngalert/api/util.go | 109 ++++++++++++++++++++++++ 8 files changed, 239 insertions(+), 112 deletions(-) create mode 100644 pkg/services/ngalert/api/forked_prom.go create mode 100644 pkg/services/ngalert/api/lotex_prom.go diff --git a/go.mod b/go.mod index 905bcae70d4..f3dac4b41e6 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/google/go-cmp v0.5.5 github.com/google/uuid v1.2.0 github.com/gosimple/slug v1.9.0 - github.com/grafana/alerting-api v0.0.0-20210323142651-d6515052e2f0 + github.com/grafana/alerting-api v0.0.0-20210323194814-03a29a4c4c27 github.com/grafana/grafana-aws-sdk v0.2.0 github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 github.com/grafana/grafana-plugin-sdk-go v0.89.0 diff --git a/go.sum b/go.sum index f1c2554fbc1..2f810eaa256 100644 --- a/go.sum +++ b/go.sum @@ -797,12 +797,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= -github.com/grafana/alerting-api v0.0.0-20210318231719-9499804fc548 h1:KjyaZJhPJ15Ul/+OQr8mbO7kDpU5i7G3r5FGVZKClTQ= -github.com/grafana/alerting-api v0.0.0-20210318231719-9499804fc548/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= -github.com/grafana/alerting-api v0.0.0-20210323141138-8873de5bf07a h1:OGKDRdmQSXKFJelrJUf9O8Xh0C8u+OQG1NSurcBYpOI= -github.com/grafana/alerting-api v0.0.0-20210323141138-8873de5bf07a/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= -github.com/grafana/alerting-api v0.0.0-20210323142651-d6515052e2f0 h1:bMYGd71RigZvkLmcdedGdMDJXKJ20luqEQLbqjgAAjI= -github.com/grafana/alerting-api v0.0.0-20210323142651-d6515052e2f0/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= +github.com/grafana/alerting-api v0.0.0-20210323194814-03a29a4c4c27 h1:DuyuEAHJeI+CMxIyzCVhmHcIeK+sjqberhDUfrgd3PY= +github.com/grafana/alerting-api v0.0.0-20210323194814-03a29a4c4c27/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= github.com/grafana/grafana v1.9.2-0.20210308201921-4ce0a49eac03/go.mod h1:AHRRvd4utJGY25J5nW8aL7wZzn/LcJ0z2za9oOp14j4= github.com/grafana/grafana-aws-sdk v0.1.0/go.mod h1:+pPo5U+pX0zWimR7YBc7ASeSQfbRkcTyQYqMiAj7G5U= github.com/grafana/grafana-aws-sdk v0.2.0 h1:UTBBYwye+ad5YUIlwN7TGxLdz1wXN3Ezhl0pseDGRVA= diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index e02c469c882..2bc646e0dd3 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -45,11 +45,18 @@ type API struct { // RegisterAPIEndpoints registers API handlers func (api *API) RegisterAPIEndpoints() { logger := log.New("ngalert.api") + proxy := &AlertingProxy{ + DataProxy: api.DataProxy, + } api.RegisterAlertmanagerApiEndpoints(AlertmanagerApiMock{log: logger}) - api.RegisterPrometheusApiEndpoints(PrometheusApiMock{log: logger}) + api.RegisterPrometheusApiEndpoints(NewForkedProm( + api.DatasourceCache, + NewLotexProm(proxy, logger), + PrometheusApiMock{log: logger}, + )) api.RegisterRulerApiEndpoints(NewForkedRuler( api.DatasourceCache, - &LotexRuler{DataProxy: api.DataProxy, log: logger}, + NewLotexRuler(proxy, logger), RulerApiMock{log: logger}, )) api.RegisterTestingApiEndpoints(TestingApiMock{log: logger}) diff --git a/pkg/services/ngalert/api/fork_ruler.go b/pkg/services/ngalert/api/fork_ruler.go index b6fcea69d9a..a8cf29f459b 100644 --- a/pkg/services/ngalert/api/fork_ruler.go +++ b/pkg/services/ngalert/api/fork_ruler.go @@ -2,7 +2,6 @@ package api import ( "fmt" - "strconv" apimodels "github.com/grafana/alerting-api/pkg/api" "github.com/grafana/grafana/pkg/api/response" @@ -24,26 +23,8 @@ func NewForkedRuler(datasourceCache datasources.CacheService, lotex, grafana Rul } } -func (r *ForkedRuler) backendType(ctx *models.ReqContext) (apimodels.Backend, error) { - recipient := ctx.Params("Recipient") - if recipient == apimodels.GrafanaBackend.String() { - return apimodels.GrafanaBackend, nil - } - if datasourceID, err := strconv.ParseInt(recipient, 10, 64); err == nil { - if ds, err := r.DatasourceCache.GetDatasource(datasourceID, ctx.SignedInUser, ctx.SkipCache); err == nil { - switch ds.Type { - case "loki", "prometheus": - return apimodels.LoTexRulerBackend, nil - default: - return 0, fmt.Errorf("unexpected backend type (%v)", ds.Type) - } - } - } - return 0, fmt.Errorf("unexpected backend type (%v)", recipient) -} - func (r *ForkedRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { - t, err := r.backendType(ctx) + t, err := backendType(ctx, r.DatasourceCache) if err != nil { return response.Error(400, err.Error(), nil) } @@ -58,7 +39,7 @@ func (r *ForkedRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) re } func (r *ForkedRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { - t, err := r.backendType(ctx) + t, err := backendType(ctx, r.DatasourceCache) if err != nil { return response.Error(400, err.Error(), nil) } @@ -73,7 +54,7 @@ func (r *ForkedRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) respons } func (r *ForkedRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { - t, err := r.backendType(ctx) + t, err := backendType(ctx, r.DatasourceCache) if err != nil { return response.Error(400, err.Error(), nil) } @@ -88,7 +69,7 @@ func (r *ForkedRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) respo } func (r *ForkedRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { - t, err := r.backendType(ctx) + t, err := backendType(ctx, r.DatasourceCache) if err != nil { return response.Error(400, err.Error(), nil) } @@ -103,7 +84,7 @@ func (r *ForkedRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response. } func (r *ForkedRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { - t, err := r.backendType(ctx) + t, err := backendType(ctx, r.DatasourceCache) if err != nil { return response.Error(400, err.Error(), nil) } @@ -118,7 +99,7 @@ func (r *ForkedRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Respo } func (r *ForkedRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.RuleGroupConfig) response.Response { - backendType, err := r.backendType(ctx) + backendType, err := backendType(ctx, r.DatasourceCache) if err != nil { return response.Error(400, err.Error(), nil) } diff --git a/pkg/services/ngalert/api/forked_prom.go b/pkg/services/ngalert/api/forked_prom.go new file mode 100644 index 00000000000..b730f6cf1a8 --- /dev/null +++ b/pkg/services/ngalert/api/forked_prom.go @@ -0,0 +1,55 @@ +package api + +import ( + "fmt" + + apimodels "github.com/grafana/alerting-api/pkg/api" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/datasources" +) + +type ForkedPromSvc struct { + ProxySvc, GrafanaSvc PrometheusApiService + DatasourceCache datasources.CacheService +} + +func NewForkedProm(datasourceCache datasources.CacheService, proxy, grafana PrometheusApiService) *ForkedPromSvc { + return &ForkedPromSvc{ + ProxySvc: proxy, + GrafanaSvc: grafana, + DatasourceCache: datasourceCache, + } +} + +func (p *ForkedPromSvc) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { + t, err := backendType(ctx, p.DatasourceCache) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + switch t { + case apimodels.GrafanaBackend: + return p.GrafanaSvc.RouteGetAlertStatuses(ctx) + case apimodels.LoTexRulerBackend: + return p.ProxySvc.RouteGetAlertStatuses(ctx) + default: + return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil) + } +} + +func (p *ForkedPromSvc) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { + t, err := backendType(ctx, p.DatasourceCache) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + switch t { + case apimodels.GrafanaBackend: + return p.GrafanaSvc.RouteGetRuleStatuses(ctx) + case apimodels.LoTexRulerBackend: + return p.ProxySvc.RouteGetRuleStatuses(ctx) + default: + return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil) + } +} diff --git a/pkg/services/ngalert/api/lotex.go b/pkg/services/ngalert/api/lotex.go index b7d4cee84e0..8f1968de3e7 100644 --- a/pkg/services/ngalert/api/lotex.go +++ b/pkg/services/ngalert/api/lotex.go @@ -2,7 +2,6 @@ package api import ( "bytes" - "encoding/json" "fmt" "io" "io/ioutil" @@ -10,96 +9,25 @@ import ( "net/url" apimodels "github.com/grafana/alerting-api/pkg/api" - "gopkg.in/macaron.v1" "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/datasourceproxy" ) const legacyRulerPrefix = "/api/prom/rules" type LotexRuler struct { - DataProxy *datasourceproxy.DatasourceProxyService - log log.Logger + log log.Logger + *AlertingProxy } -// macaron unsafely asserts the http.ResponseWriter is an http.CloseNotifier, which will panic. -// Here we impl it, which will ensure this no longer happens, but neither will we take -// advantage cancelling upstream requests when the downstream has closed. -// NB: http.CloseNotifier is a deprecated ifc from before the context pkg. -type safeMacaronWrapper struct { - http.ResponseWriter -} - -func (w *safeMacaronWrapper) CloseNotify() <-chan bool { - return make(chan bool) -} - -// replacedResponseWriter overwrites the underlying responsewriter used by a *models.ReqContext. -// It's ugly because it needs to replace a value behind a few nested pointers. -func replacedResponseWriter(ctx *models.ReqContext) (*models.ReqContext, *response.NormalResponse) { - resp := response.CreateNormalResponse(make(http.Header), nil, 0) - cpy := *ctx - cpyMCtx := *cpy.Context - cpyMCtx.Resp = macaron.NewResponseWriter(ctx.Req.Method, &safeMacaronWrapper{resp}) - cpy.Context = &cpyMCtx - return &cpy, resp -} - -// withReq proxies a different request -func (r *LotexRuler) withReq( - ctx *models.ReqContext, - req *http.Request, - extractor func([]byte) (interface{}, error), -) response.Response { - newCtx, resp := replacedResponseWriter(ctx) - newCtx.Req.Request = req - r.DataProxy.ProxyDatasourceRequestWithID(newCtx, ctx.ParamsInt64("Recipient")) - - status := resp.Status() - if status >= 400 { - return response.Error(status, string(resp.Body()), nil) +func NewLotexRuler(proxy *AlertingProxy, log log.Logger) *LotexRuler { + return &LotexRuler{ + log: log, + AlertingProxy: proxy, } - - t, err := extractor(resp.Body()) - if err != nil { - return response.Error(500, err.Error(), nil) - } - - b, err := json.Marshal(t) - if err != nil { - return response.Error(500, err.Error(), nil) - } - - return response.JSON(status, b) -} - -func yamlExtractor(v interface{}) func([]byte) (interface{}, error) { - return func(b []byte) (interface{}, error) { - decoder := yaml.NewDecoder(bytes.NewReader(b)) - decoder.KnownFields(true) - - err := decoder.Decode(v) - - return v, err - } -} - -func jsonExtractor(v interface{}) func([]byte) (interface{}, error) { - if v == nil { - // json unmarshal expects a pointer - v = &map[string]interface{}{} - } - return func(b []byte) (interface{}, error) { - return v, json.Unmarshal(b, v) - } -} - -func messageExtractor(b []byte) (interface{}, error) { - return map[string]string{"message": string(b)}, nil } func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { diff --git a/pkg/services/ngalert/api/lotex_prom.go b/pkg/services/ngalert/api/lotex_prom.go new file mode 100644 index 00000000000..825ca91a870 --- /dev/null +++ b/pkg/services/ngalert/api/lotex_prom.go @@ -0,0 +1,51 @@ +package api + +import ( + "net/http" + + apimodels "github.com/grafana/alerting-api/pkg/api" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" +) + +const ( + promRulesPath = "/prometheus/api/v1/rules" + promAlertsPath = "/prometheus/api/v1/alerts" +) + +type LotexProm struct { + log log.Logger + *AlertingProxy +} + +func NewLotexProm(proxy *AlertingProxy, log log.Logger) *LotexProm { + return &LotexProm{ + log: log, + AlertingProxy: proxy, + } +} + +func (p *LotexProm) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { + return p.withReq( + ctx, &http.Request{ + URL: withPath( + *ctx.Req.URL, + promAlertsPath, + ), + }, + jsonExtractor(&apimodels.AlertResponse{}), + ) +} + +func (p *LotexProm) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { + return p.withReq( + ctx, &http.Request{ + URL: withPath( + *ctx.Req.URL, + promRulesPath, + ), + }, + jsonExtractor(&apimodels.RuleResponse{}), + ) +} diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index 263ec981d1c..6a5b08b9a01 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -1,10 +1,21 @@ package api import ( + "bytes" + "encoding/json" "fmt" + "net/http" "regexp" + "strconv" "github.com/go-openapi/strfmt" + apimodels "github.com/grafana/alerting-api/pkg/api" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/datasourceproxy" + "github.com/grafana/grafana/pkg/services/datasources" + "gopkg.in/macaron.v1" + "gopkg.in/yaml.v3" ) var searchRegex = regexp.MustCompile(`\{(\w+)\}`) @@ -27,3 +38,101 @@ func stringPtr(s string) *string { func boolPtr(b bool) *bool { return &b } + +func backendType(ctx *models.ReqContext, cache datasources.CacheService) (apimodels.Backend, error) { + recipient := ctx.Params("Recipient") + if recipient == apimodels.GrafanaBackend.String() { + return apimodels.GrafanaBackend, nil + } + if datasourceID, err := strconv.ParseInt(recipient, 10, 64); err == nil { + if ds, err := cache.GetDatasource(datasourceID, ctx.SignedInUser, ctx.SkipCache); err == nil { + switch ds.Type { + case "loki", "prometheus": + return apimodels.LoTexRulerBackend, nil + default: + return 0, fmt.Errorf("unexpected backend type (%v)", ds.Type) + } + } + } + return 0, fmt.Errorf("unexpected backend type (%v)", recipient) +} + +// macaron unsafely asserts the http.ResponseWriter is an http.CloseNotifier, which will panic. +// Here we impl it, which will ensure this no longer happens, but neither will we take +// advantage cancelling upstream requests when the downstream has closed. +// NB: http.CloseNotifier is a deprecated ifc from before the context pkg. +type safeMacaronWrapper struct { + http.ResponseWriter +} + +func (w *safeMacaronWrapper) CloseNotify() <-chan bool { + return make(chan bool) +} + +// replacedResponseWriter overwrites the underlying responsewriter used by a *models.ReqContext. +// It's ugly because it needs to replace a value behind a few nested pointers. +func replacedResponseWriter(ctx *models.ReqContext) (*models.ReqContext, *response.NormalResponse) { + resp := response.CreateNormalResponse(make(http.Header), nil, 0) + cpy := *ctx + cpyMCtx := *cpy.Context + cpyMCtx.Resp = macaron.NewResponseWriter(ctx.Req.Method, &safeMacaronWrapper{resp}) + cpy.Context = &cpyMCtx + return &cpy, resp +} + +type AlertingProxy struct { + DataProxy *datasourceproxy.DatasourceProxyService +} + +// withReq proxies a different request +func (p *AlertingProxy) withReq( + ctx *models.ReqContext, + req *http.Request, + extractor func([]byte) (interface{}, error), +) response.Response { + newCtx, resp := replacedResponseWriter(ctx) + newCtx.Req.Request = req + p.DataProxy.ProxyDatasourceRequestWithID(newCtx, ctx.ParamsInt64("Recipient")) + + status := resp.Status() + if status >= 400 { + return response.Error(status, string(resp.Body()), nil) + } + + t, err := extractor(resp.Body()) + if err != nil { + return response.Error(500, err.Error(), nil) + } + + b, err := json.Marshal(t) + if err != nil { + return response.Error(500, err.Error(), nil) + } + + return response.JSON(status, b) +} + +func yamlExtractor(v interface{}) func([]byte) (interface{}, error) { + return func(b []byte) (interface{}, error) { + decoder := yaml.NewDecoder(bytes.NewReader(b)) + decoder.KnownFields(true) + + err := decoder.Decode(v) + + return v, err + } +} + +func jsonExtractor(v interface{}) func([]byte) (interface{}, error) { + if v == nil { + // json unmarshal expects a pointer + v = &map[string]interface{}{} + } + return func(b []byte) (interface{}, error) { + return v, json.Unmarshal(b, v) + } +} + +func messageExtractor(b []byte) (interface{}, error) { + return map[string]string{"message": string(b)}, nil +}