mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[Alerting]: Implement test rule API route (#32837)
* [Alerting]: Implement test rule API route * Apply suggestions from code review * Call /query instead of /query_range
This commit is contained in:
parent
15978900a9
commit
e7ff04a167
2
go.sum
2
go.sum
@ -1044,6 +1044,7 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E
|
|||||||
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
||||||
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
||||||
github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=
|
github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=
|
||||||
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||||
@ -1450,6 +1451,7 @@ github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17 h1:VN3p3Nb
|
|||||||
github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17/go.mod h1:dv3B1syqmkrkmo665MPCU6L8PbTXIiUeg/OEQULLNxA=
|
github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17/go.mod h1:dv3B1syqmkrkmo665MPCU6L8PbTXIiUeg/OEQULLNxA=
|
||||||
github.com/prometheus/statsd_exporter v0.15.0/go.mod h1:Dv8HnkoLQkeEjkIE4/2ndAA7WL1zHKK7WMqFQqu72rw=
|
github.com/prometheus/statsd_exporter v0.15.0/go.mod h1:Dv8HnkoLQkeEjkIE4/2ndAA7WL1zHKK7WMqFQqu72rw=
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 h1:eL7x4/zMnlquMxYe7V078BD7MGskZ0daGln+SJCVzuY=
|
||||||
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3/go.mod h1:P7JlQWFT7jDcFZMtUPQbtGzzzxva3rBn6oIF+LPwFcM=
|
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3/go.mod h1:P7JlQWFT7jDcFZMtUPQbtGzzzxva3rBn6oIF+LPwFcM=
|
||||||
github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY=
|
github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY=
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||||
|
@ -77,8 +77,13 @@ func (api *API) RegisterAPIEndpoints() {
|
|||||||
NewLotexRuler(proxy, logger),
|
NewLotexRuler(proxy, logger),
|
||||||
RulerSrv{store: api.RuleStore, log: logger},
|
RulerSrv{store: api.RuleStore, log: logger},
|
||||||
))
|
))
|
||||||
// Register endpoints for testing evaluation of rules and notification channels.
|
api.RegisterTestingApiEndpoints(TestingApiSrv{
|
||||||
api.RegisterTestingApiEndpoints(TestingApiMock{log: logger})
|
AlertingProxy: proxy,
|
||||||
|
Cfg: api.Cfg,
|
||||||
|
DataService: api.DataService,
|
||||||
|
DatasourceCache: api.DatasourceCache,
|
||||||
|
log: logger,
|
||||||
|
})
|
||||||
|
|
||||||
// Legacy routes; they will be removed in v8
|
// Legacy routes; they will be removed in v8
|
||||||
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
|
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
|
||||||
|
79
pkg/services/ngalert/api/api_testing.go
Normal file
79
pkg/services/ngalert/api/api_testing.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
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"
|
||||||
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestingApiSrv struct {
|
||||||
|
*AlertingProxy
|
||||||
|
Cfg *setting.Cfg
|
||||||
|
DataService *tsdb.Service
|
||||||
|
DatasourceCache datasources.CacheService
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv TestingApiSrv) RouteTestReceiverConfig(c *models.ReqContext, body apimodels.ExtendedReceiver) response.Response {
|
||||||
|
srv.log.Info("RouteTestReceiverConfig: ", "body", body)
|
||||||
|
return response.JSON(http.StatusOK, util.DynMap{"message": "success"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv TestingApiSrv) RouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response {
|
||||||
|
recipient := c.Params("Recipient")
|
||||||
|
if recipient == apimodels.GrafanaBackend.String() {
|
||||||
|
if body.Type() != apimodels.GrafanaBackend || body.GrafanaManagedCondition == nil {
|
||||||
|
return response.Error(http.StatusBadRequest, "unexpected payload", nil)
|
||||||
|
}
|
||||||
|
return conditionEval(c, *body.GrafanaManagedCondition, srv.DatasourceCache, srv.DataService, srv.Cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.Type() != apimodels.LoTexRulerBackend {
|
||||||
|
return response.Error(http.StatusBadRequest, "unexpected payload", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var path string
|
||||||
|
if datasourceID, err := strconv.ParseInt(recipient, 10, 64); err == nil {
|
||||||
|
ds, err := srv.DatasourceCache.GetDatasource(datasourceID, c.SignedInUser, c.SkipCache)
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(http.StatusInternalServerError, "failed to get datasource", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ds.Type {
|
||||||
|
case "loki":
|
||||||
|
path = "loki/api/v1/query"
|
||||||
|
case "prometheus":
|
||||||
|
path = "api/v1/query"
|
||||||
|
default:
|
||||||
|
return response.Error(http.StatusBadRequest, fmt.Sprintf("unexpected recipient type %s", ds.Type), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t := timeNow()
|
||||||
|
queryURL, err := url.Parse(path)
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(http.StatusInternalServerError, "failed to parse url", err)
|
||||||
|
}
|
||||||
|
params := queryURL.Query()
|
||||||
|
params.Set("query", body.Expr)
|
||||||
|
params.Set("time", strconv.FormatInt(t.Unix(), 10))
|
||||||
|
queryURL.RawQuery = params.Encode()
|
||||||
|
return srv.withReq(
|
||||||
|
c,
|
||||||
|
http.MethodGet,
|
||||||
|
queryURL,
|
||||||
|
nil,
|
||||||
|
jsonExtractor(nil),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
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"
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TestingApiMock struct {
|
|
||||||
log log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mock TestingApiMock) RouteTestReceiverConfig(c *models.ReqContext, body apimodels.ExtendedReceiver) response.Response {
|
|
||||||
mock.log.Info("RouteTestReceiverConfig: ", "body", body)
|
|
||||||
return response.JSON(http.StatusOK, util.DynMap{"message": "success"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mock TestingApiMock) RouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response {
|
|
||||||
mock.log.Info("RouteTestRuleConfig: ", "body", body)
|
|
||||||
result := apimodels.TestRuleResponse{
|
|
||||||
GrafanaAlertInstances: apimodels.AlertInstancesResponse{
|
|
||||||
Instances: [][]byte{
|
|
||||||
[]byte("QVJST1cxAAD/////+AAAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAACE////CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAKT///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAAQAAABgAAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAAQAAAAEQAAAAAAAAGQAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAAAAAAAAAAA/////4gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAIAAAAAAAAABQAAAAAAAADAwAKABgADAAIAAQACgAAABQAAAA4AAAAAQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAPAAAAAAAAwABAAAACAEAAAAAAACQAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAABQAAAAAgAAACgAAAAEAAAAhP///wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACk////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAEAAAAYAAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEAAAABEAAAAAAAABkAAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAAAAAAABAAEAAQAAAAAAAAAAAAAACgBAABBUlJPVzE="),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return response.JSON(http.StatusOK, result)
|
|
||||||
}
|
|
@ -23,7 +23,7 @@ type TestingApiService interface {
|
|||||||
|
|
||||||
func (api *API) RegisterTestingApiEndpoints(srv TestingApiService) {
|
func (api *API) RegisterTestingApiEndpoints(srv TestingApiService) {
|
||||||
api.RouteRegister.Group("", func(group routing.RouteRegister) {
|
api.RouteRegister.Group("", func(group routing.RouteRegister) {
|
||||||
group.Post(toMacaronPath("/api/v1/receiver/test"), binding.Bind(apimodels.ExtendedReceiver{}), routing.Wrap(srv.RouteTestReceiverConfig))
|
group.Post(toMacaronPath("/api/v1/receiver/test/{Recipient}"), binding.Bind(apimodels.ExtendedReceiver{}), routing.Wrap(srv.RouteTestReceiverConfig))
|
||||||
group.Post(toMacaronPath("/api/v1/rule/test"), binding.Bind(apimodels.TestRulePayload{}), routing.Wrap(srv.RouteTestRuleConfig))
|
group.Post(toMacaronPath("/api/v1/rule/test/{Recipient}"), binding.Bind(apimodels.TestRulePayload{}), routing.Wrap(srv.RouteTestRuleConfig))
|
||||||
}, middleware.ReqSignedIn)
|
}, middleware.ReqSignedIn)
|
||||||
}
|
}
|
||||||
|
@ -24,31 +24,7 @@ func (api *API) listAlertInstancesEndpoint(c *models.ReqContext) response.Respon
|
|||||||
|
|
||||||
// conditionEvalEndpoint handles POST /api/alert-definitions/eval.
|
// conditionEvalEndpoint handles POST /api/alert-definitions/eval.
|
||||||
func (api *API) conditionEvalEndpoint(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand) response.Response {
|
func (api *API) conditionEvalEndpoint(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand) response.Response {
|
||||||
evalCond := ngmodels.Condition{
|
return conditionEval(c, cmd, api.DatasourceCache, api.DataService, api.Cfg)
|
||||||
Condition: cmd.Condition,
|
|
||||||
OrgID: c.SignedInUser.OrgId,
|
|
||||||
Data: cmd.Data,
|
|
||||||
}
|
|
||||||
if err := api.validateCondition(evalCond, c.SignedInUser, c.SkipCache); err != nil {
|
|
||||||
return response.Error(400, "invalid condition", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := cmd.Now
|
|
||||||
if now.IsZero() {
|
|
||||||
now = timeNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluator := eval.Evaluator{Cfg: api.Cfg}
|
|
||||||
evalResults, err := evaluator.ConditionEval(&evalCond, timeNow(), api.DataService)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "Failed to evaluate conditions", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
frame := evalResults.AsDataFrame()
|
|
||||||
|
|
||||||
return response.JSONStreaming(200, util.DynMap{
|
|
||||||
"instances": []*data.Frame{&frame},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:alertDefinitionUID.
|
// alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:alertDefinitionUID.
|
||||||
@ -60,7 +36,7 @@ func (api *API) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Respo
|
|||||||
return response.Error(400, "Failed to load alert definition conditions", err)
|
return response.Error(400, "Failed to load alert definition conditions", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.validateCondition(*condition, c.SignedInUser, c.SkipCache); err != nil {
|
if err := validateCondition(*condition, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
||||||
return response.Error(400, "invalid condition", err)
|
return response.Error(400, "invalid condition", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +94,8 @@ func (api *API) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels
|
|||||||
OrgID: c.SignedInUser.OrgId,
|
OrgID: c.SignedInUser.OrgId,
|
||||||
Data: cmd.Data,
|
Data: cmd.Data,
|
||||||
}
|
}
|
||||||
if err := api.validateCondition(evalCond, c.SignedInUser, c.SkipCache); err != nil {
|
|
||||||
|
if err := validateCondition(evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
||||||
return response.Error(400, "invalid condition", err)
|
return response.Error(400, "invalid condition", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +115,8 @@ func (api *API) createAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels
|
|||||||
OrgID: c.SignedInUser.OrgId,
|
OrgID: c.SignedInUser.OrgId,
|
||||||
Data: cmd.Data,
|
Data: cmd.Data,
|
||||||
}
|
}
|
||||||
if err := api.validateCondition(evalCond, c.SignedInUser, c.SkipCache); err != nil {
|
|
||||||
|
if err := validateCondition(evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
||||||
return response.Error(400, "invalid condition", err)
|
return response.Error(400, "invalid condition", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,43 +198,6 @@ func (api *API) LoadAlertCondition(alertDefinitionUID string, orgID int64) (*ngm
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) validateCondition(c ngmodels.Condition, user *models.SignedInUser, skipCache bool) error {
|
|
||||||
var refID string
|
|
||||||
|
|
||||||
if len(c.Data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, query := range c.Data {
|
|
||||||
if c.Condition == query.RefID {
|
|
||||||
refID = c.Condition
|
|
||||||
}
|
|
||||||
|
|
||||||
datasourceUID, err := query.GetDatasource()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
isExpression, err := query.IsExpression()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if isExpression {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = api.DatasourceCache.GetDatasourceByUID(datasourceUID, user, skipCache)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get datasource: %s: %w", datasourceUID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if refID == "" {
|
|
||||||
return fmt.Errorf("condition %s not found in any query or expression", c.Condition)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) validateOrgAlertDefinition(c *models.ReqContext) {
|
func (api *API) validateOrgAlertDefinition(c *models.ReqContext) {
|
||||||
uid := c.ParamsEscape(":alertDefinitionUID")
|
uid := c.ParamsEscape(":alertDefinitionUID")
|
||||||
|
|
||||||
|
@ -28,7 +28,8 @@ func (api *API) conditionEvalOldEndpoint(c *models.ReqContext) response.Response
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return response.Error(400, "Failed to translate alert conditions", err)
|
return response.Error(400, "Failed to translate alert conditions", err)
|
||||||
}
|
}
|
||||||
if err := api.validateCondition(*evalCond, c.SignedInUser, c.SkipCache); err != nil {
|
|
||||||
|
if err := validateCondition(*evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
||||||
return response.Error(400, "invalid condition", err)
|
return response.Error(400, "invalid condition", err)
|
||||||
}
|
}
|
||||||
//now := cmd.Now
|
//now := cmd.Now
|
||||||
@ -70,7 +71,8 @@ func (api *API) conditionEvalOldEndpointByID(c *models.ReqContext) response.Resp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return response.Error(400, "Failed to translate alert conditions", err)
|
return response.Error(400, "Failed to translate alert conditions", err)
|
||||||
}
|
}
|
||||||
if err := api.validateCondition(*evalCond, c.SignedInUser, c.SkipCache); err != nil {
|
|
||||||
|
if err := validateCondition(*evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
||||||
return response.Error(400, "invalid condition", err)
|
return response.Error(400, "invalid condition", err)
|
||||||
}
|
}
|
||||||
//now := cmd.Now
|
//now := cmd.Now
|
||||||
|
86
pkg/services/ngalert/api/test-data/test.http
Normal file
86
pkg/services/ngalert/api/test-data/test.http
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
@grafanaRecipient = grafana
|
||||||
|
@lokiDatasourceID = 32
|
||||||
|
@prometheusDatasourceID = 35
|
||||||
|
|
||||||
|
POST http://admin:admin@localhost:3000/api/v1/rule/test/{{grafanaRecipient}}
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"grafana_condition": {
|
||||||
|
"condition": "A",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"relativeTimeRange": {
|
||||||
|
"from": 18000,
|
||||||
|
"to": 10800
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"datasource": "__expr__",
|
||||||
|
"type":"math",
|
||||||
|
"expression":"1 < 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
POST http://admin:admin@localhost:3000/api/v1/rule/test/{{lokiDatasourceID}}
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"expr": "rate({cluster=\"us-central1\", job=\"loki-prod/loki-canary\"}[1m]) > 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
POST http://admin:admin@localhost:3000/api/v1/rule/test/{{prometheusDatasourceID}}
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"expr": "http_request_duration_microseconds > 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
### loki recipient - empty payload
|
||||||
|
POST http://admin:admin@localhost:3000/api/v1/rule/test/{{lokiDatasourceID}}
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
### grafana recipient - empty payload
|
||||||
|
POST http://admin:admin@localhost:3000/api/v1/rule/test/{{grafanaRecipient}}
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
### loki recipient - grafana payload
|
||||||
|
POST http://admin:admin@localhost:3000/api/v1/rule/test/{{lokiDatasourceID}}
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"grafana_condition": {
|
||||||
|
"condition": "A",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"relativeTimeRange": {
|
||||||
|
"from": 18000,
|
||||||
|
"to": 10800
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"datasource": "__expr__",
|
||||||
|
"type":"math",
|
||||||
|
"expression":"1 < 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
### grafana recipient - lotex payload
|
||||||
|
POST http://admin:admin@localhost:3000/api/v1/rule/test/{{grafanaRecipient}}
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"expr": "rate({cluster=\"us-central1\", job=\"loki-prod/loki-canary\"}[1m]) > 0"
|
||||||
|
}
|
@ -12,10 +12,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
apimodels "github.com/grafana/alerting-api/pkg/api"
|
apimodels "github.com/grafana/alerting-api/pkg/api"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||||
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@ -150,3 +156,68 @@ func jsonExtractor(v interface{}) func([]byte) (interface{}, error) {
|
|||||||
func messageExtractor(b []byte) (interface{}, error) {
|
func messageExtractor(b []byte) (interface{}, error) {
|
||||||
return map[string]string{"message": string(b)}, nil
|
return map[string]string{"message": string(b)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateCondition(c ngmodels.Condition, user *models.SignedInUser, skipCache bool, datasourceCache datasources.CacheService) error {
|
||||||
|
var refID string
|
||||||
|
|
||||||
|
if len(c.Data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, query := range c.Data {
|
||||||
|
if c.Condition == query.RefID {
|
||||||
|
refID = c.Condition
|
||||||
|
}
|
||||||
|
|
||||||
|
datasourceUID, err := query.GetDatasource()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isExpression, err := query.IsExpression()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isExpression {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = datasourceCache.GetDatasourceByUID(datasourceUID, user, skipCache)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get datasource: %s: %w", datasourceUID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if refID == "" {
|
||||||
|
return fmt.Errorf("condition %s not found in any query or expression", c.Condition)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func conditionEval(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand, datasourceCache datasources.CacheService, dataService *tsdb.Service, cfg *setting.Cfg) response.Response {
|
||||||
|
evalCond := ngmodels.Condition{
|
||||||
|
Condition: cmd.Condition,
|
||||||
|
OrgID: c.SignedInUser.OrgId,
|
||||||
|
Data: cmd.Data,
|
||||||
|
}
|
||||||
|
if err := validateCondition(evalCond, c.SignedInUser, c.SkipCache, datasourceCache); err != nil {
|
||||||
|
return response.Error(400, "invalid condition", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := cmd.Now
|
||||||
|
if now.IsZero() {
|
||||||
|
now = timeNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluator := eval.Evaluator{Cfg: cfg}
|
||||||
|
evalResults, err := evaluator.ConditionEval(&evalCond, timeNow(), dataService)
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(400, "Failed to evaluate conditions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := evalResults.AsDataFrame()
|
||||||
|
|
||||||
|
return response.JSONStreaming(200, util.DynMap{
|
||||||
|
"instances": []*data.Frame{&frame},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -165,7 +165,7 @@ func execute(ctx AlertExecCtx, c *models.Condition, now time.Time, dataService *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(result.Results) == 0 {
|
if len(result.Results) == 0 {
|
||||||
err = fmt.Errorf("no GEL results")
|
err = fmt.Errorf("no transformation results")
|
||||||
result.Error = err
|
result.Error = err
|
||||||
return &result, err
|
return &result, err
|
||||||
}
|
}
|
||||||
|
35
pkg/services/ngalert/models/eval_condition.go
Normal file
35
pkg/services/ngalert/models/eval_condition.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EvalAlertConditionCommand is the command for evaluating a condition
|
||||||
|
type EvalAlertConditionCommand struct {
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
Data []AlertQuery `json:"data"`
|
||||||
|
Now time.Time `json:"now"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *EvalAlertConditionCommand) UnmarshalJSON(b []byte) error {
|
||||||
|
type plain EvalAlertConditionCommand
|
||||||
|
if err := json.Unmarshal(b, (*plain)(cmd)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *EvalAlertConditionCommand) validate() error {
|
||||||
|
if cmd.Condition == "" {
|
||||||
|
return fmt.Errorf("missing condition")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmd.Data) == 0 {
|
||||||
|
return fmt.Errorf("missing data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -131,11 +131,3 @@ type UpdateAlertDefinitionPausedCommand struct {
|
|||||||
|
|
||||||
ResultCount int64
|
ResultCount int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvalAlertConditionCommand is the command for evaluating a condition
|
|
||||||
// Legacy model; It will be removed in v8
|
|
||||||
type EvalAlertConditionCommand struct {
|
|
||||||
Condition string `json:"condition"`
|
|
||||||
Data []AlertQuery `json:"data"`
|
|
||||||
Now time.Time `json:"now"`
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user