[Alerting] Forking LoTex ruler (#32138)

* updates alerting api to master

* skeleton for lotex ruler

* withPath helper & legacyRulerPrefix const

* forked ruler

* wires up proxy

* safeMacaronWrapper

* working proxy

* jsonExtractor

* lint
This commit is contained in:
Owen Diehl 2021-03-19 10:32:13 -04:00 committed by GitHub
parent c8b59b79c3
commit 93d0f7163f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 337 additions and 10 deletions

3
go.mod
View File

@ -40,7 +40,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-20210316151414-4987b85e57ee
github.com/grafana/alerting-api v0.0.0-20210318231719-9499804fc548
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.88.0
@ -97,6 +97,7 @@ require (
gopkg.in/redis.v5 v5.2.9
gopkg.in/square/go-jose.v2 v2.5.1
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
xorm.io/core v0.7.3
xorm.io/xorm v0.8.2
)

4
go.sum
View File

@ -794,8 +794,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-20210316151414-4987b85e57ee h1:LZo5p1wyV4RbQa28QBJZoS+VtnH1ruh4K4k1fOuHxJs=
github.com/grafana/alerting-api v0.0.0-20210316151414-4987b85e57ee/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY=
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/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=

View File

@ -34,7 +34,6 @@ type NormalResponse struct {
header http.Header
errMessage string
err error
http.ResponseWriter
}
// Write implements http.ResponseWriter

View File

@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
@ -36,6 +37,7 @@ type API struct {
DataService *tsdb.Service
Schedule schedule.ScheduleService
Store store.Store
DataProxy *datasourceproxy.DatasourceProxyService
}
// RegisterAPIEndpoints registers API handlers
@ -43,7 +45,10 @@ func (api *API) RegisterAPIEndpoints() {
logger := log.New("ngalert.api")
api.RegisterAlertmanagerApiEndpoints(AlertmanagerApiMock{log: logger})
api.RegisterPrometheusApiEndpoints(PrometheusApiMock{log: logger})
api.RegisterRulerApiEndpoints(RulerApiMock{log: logger})
api.RegisterRulerApiEndpoints(NewForkedRuler(
&LotexRuler{DataProxy: api.DataProxy, log: logger},
RulerApiMock{log: logger},
))
api.RegisterTestingApiEndpoints(TestingApiMock{log: logger})
// Legacy routes; they will be removed in v8

View File

@ -0,0 +1,107 @@
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"
)
// ForkedRuler will validate and proxy requests to the correct backend type depending on the datasource.
type ForkedRuler struct {
LotexRuler, GrafanaRuler RulerApiService
}
func NewForkedRuler(lotex, grafana RulerApiService) *ForkedRuler {
return &ForkedRuler{
LotexRuler: lotex,
GrafanaRuler: grafana,
}
}
func (r *ForkedRuler) backendType(ctx *models.ReqContext) apimodels.Backend {
// TODO: implement, hardcoded for now
return apimodels.LoTexRulerBackend
}
func (r *ForkedRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response {
switch t := r.backendType(ctx); t {
case apimodels.GrafanaBackend:
return r.GrafanaRuler.RouteDeleteNamespaceRulesConfig(ctx)
case apimodels.LoTexRulerBackend:
return r.LotexRuler.RouteDeleteNamespaceRulesConfig(ctx)
default:
return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil)
}
}
func (r *ForkedRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response {
switch t := r.backendType(ctx); t {
case apimodels.GrafanaBackend:
return r.GrafanaRuler.RouteDeleteRuleGroupConfig(ctx)
case apimodels.LoTexRulerBackend:
return r.LotexRuler.RouteDeleteRuleGroupConfig(ctx)
default:
return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil)
}
}
func (r *ForkedRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response {
switch t := r.backendType(ctx); t {
case apimodels.GrafanaBackend:
return r.GrafanaRuler.RouteGetNamespaceRulesConfig(ctx)
case apimodels.LoTexRulerBackend:
return r.LotexRuler.RouteGetNamespaceRulesConfig(ctx)
default:
return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil)
}
}
func (r *ForkedRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response {
switch t := r.backendType(ctx); t {
case apimodels.GrafanaBackend:
return r.GrafanaRuler.RouteGetRulegGroupConfig(ctx)
case apimodels.LoTexRulerBackend:
return r.LotexRuler.RouteGetRulegGroupConfig(ctx)
default:
return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil)
}
}
func (r *ForkedRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response {
switch t := r.backendType(ctx); t {
case apimodels.GrafanaBackend:
return r.GrafanaRuler.RouteGetRulesConfig(ctx)
case apimodels.LoTexRulerBackend:
return r.LotexRuler.RouteGetRulesConfig(ctx)
default:
return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil)
}
}
func (r *ForkedRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.RuleGroupConfig) response.Response {
backendType := r.backendType(ctx)
payloadType := conf.Type()
if backendType != payloadType {
return response.Error(
400,
fmt.Sprintf(
"unexpected backend type (%v) vs payload type (%v)",
backendType,
payloadType,
),
nil,
)
}
switch backendType {
case apimodels.GrafanaBackend:
return r.GrafanaRuler.RoutePostNameRulesConfig(ctx, conf)
case apimodels.LoTexRulerBackend:
return r.LotexRuler.RoutePostNameRulesConfig(ctx, conf)
default:
return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", backendType), nil)
}
}

View File

@ -0,0 +1,212 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"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
}
// 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("DatasourceId"))
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
}
func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response {
return r.withReq(
ctx,
&http.Request{
Method: "DELETE",
URL: withPath(
*ctx.Req.URL,
fmt.Sprintf("/api/prom/rules/%s", ctx.Params("Namespace")),
),
},
messageExtractor,
)
}
func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response {
return r.withReq(
ctx,
&http.Request{
Method: "DELETE",
URL: withPath(
*ctx.Req.URL,
fmt.Sprintf(
"%s/%s/%s",
legacyRulerPrefix,
ctx.Params("Namespace"),
ctx.Params("Groupname"),
),
),
},
messageExtractor,
)
}
func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response {
return r.withReq(
ctx, &http.Request{
URL: withPath(
*ctx.Req.URL,
fmt.Sprintf(
"%s/%s",
legacyRulerPrefix,
ctx.Params("Namespace"),
),
),
},
yamlExtractor(apimodels.NamespaceConfigResponse{}),
)
}
func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response {
return r.withReq(
ctx,
&http.Request{
URL: withPath(
*ctx.Req.URL,
fmt.Sprintf(
"%s/%s/%s",
legacyRulerPrefix,
ctx.Params("Namespace"),
ctx.Params("Groupname"),
),
),
},
yamlExtractor(apimodels.RuleGroupConfigResponse{}),
)
}
func (r *LotexRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response {
return r.withReq(
ctx,
&http.Request{
URL: withPath(
*ctx.Req.URL,
legacyRulerPrefix,
),
},
yamlExtractor(apimodels.NamespaceConfigResponse{}),
)
}
func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.RuleGroupConfig) response.Response {
yml, err := yaml.Marshal(conf)
if err != nil {
return response.Error(500, "Failed marshal rule group", err)
}
body, ln := payload(yml)
ns := ctx.Params("Namespace")
u := withPath(*ctx.Req.URL, fmt.Sprintf("%s/%s", legacyRulerPrefix, ns))
req := &http.Request{
Method: "POST",
URL: u,
Body: body,
ContentLength: ln,
}
return r.withReq(ctx, req, jsonExtractor(nil))
}
func withPath(u url.URL, newPath string) *url.URL {
// TODO: handle path escaping
u.Path = newPath
return &u
}
func payload(b []byte) (io.ReadCloser, int64) {
return ioutil.NopCloser(bytes.NewBuffer(b)), int64(len(b))
}

View File

@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/ngalert/api"
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
@ -36,11 +37,12 @@ const (
// AlertNG is the service for evaluating the condition of an alert definition.
type AlertNG struct {
Cfg *setting.Cfg `inject:""`
DatasourceCache datasources.CacheService `inject:""`
RouteRegister routing.RouteRegister `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
DataService *tsdb.Service `inject:""`
Cfg *setting.Cfg `inject:""`
DatasourceCache datasources.CacheService `inject:""`
RouteRegister routing.RouteRegister `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
DataService *tsdb.Service `inject:""`
DataProxy *datasourceproxy.DatasourceProxyService `inject:""`
Log log.Logger
schedule schedule.ScheduleService
}
@ -73,6 +75,7 @@ func (ng *AlertNG) Init() error {
RouteRegister: ng.RouteRegister,
DataService: ng.DataService,
Schedule: ng.schedule,
DataProxy: ng.DataProxy,
Store: store}
api.RegisterAPIEndpoints()