From e7ff04a16753255b304c57dac42c9ba4ffaefe21 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Tue, 13 Apr 2021 20:58:34 +0300 Subject: [PATCH] [Alerting]: Implement test rule API route (#32837) * [Alerting]: Implement test rule API route * Apply suggestions from code review * Call /query instead of /query_range --- go.sum | 2 + pkg/services/ngalert/api/api.go | 9 +- pkg/services/ngalert/api/api_testing.go | 79 +++++++++++++++++ pkg/services/ngalert/api/api_testing_mock.go | 32 ------- .../ngalert/api/generated_base_api_testing.go | 4 +- pkg/services/ngalert/api/legacy_api_routes.go | 71 ++------------- pkg/services/ngalert/api/legacy_trans_dev.go | 6 +- pkg/services/ngalert/api/test-data/test.http | 86 +++++++++++++++++++ pkg/services/ngalert/api/util.go | 71 +++++++++++++++ pkg/services/ngalert/eval/eval.go | 2 +- pkg/services/ngalert/models/eval_condition.go | 35 ++++++++ pkg/services/ngalert/models/legacy.go | 8 -- 12 files changed, 293 insertions(+), 112 deletions(-) create mode 100644 pkg/services/ngalert/api/api_testing.go delete mode 100644 pkg/services/ngalert/api/api_testing_mock.go create mode 100644 pkg/services/ngalert/api/test-data/test.http create mode 100644 pkg/services/ngalert/models/eval_condition.go diff --git a/go.sum b/go.sum index 502adb0a827..10f9e14f2e4 100644 --- a/go.sum +++ b/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/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= 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/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= @@ -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/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/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/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index c6f7575b1ac..1f745d1bc69 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -77,8 +77,13 @@ func (api *API) RegisterAPIEndpoints() { NewLotexRuler(proxy, logger), RulerSrv{store: api.RuleStore, log: logger}, )) - // Register endpoints for testing evaluation of rules and notification channels. - api.RegisterTestingApiEndpoints(TestingApiMock{log: logger}) + api.RegisterTestingApiEndpoints(TestingApiSrv{ + AlertingProxy: proxy, + Cfg: api.Cfg, + DataService: api.DataService, + DatasourceCache: api.DatasourceCache, + log: logger, + }) // Legacy routes; they will be removed in v8 api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) { diff --git a/pkg/services/ngalert/api/api_testing.go b/pkg/services/ngalert/api/api_testing.go new file mode 100644 index 00000000000..4fd93140861 --- /dev/null +++ b/pkg/services/ngalert/api/api_testing.go @@ -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, + ) +} diff --git a/pkg/services/ngalert/api/api_testing_mock.go b/pkg/services/ngalert/api/api_testing_mock.go deleted file mode 100644 index 75058b4111e..00000000000 --- a/pkg/services/ngalert/api/api_testing_mock.go +++ /dev/null @@ -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) -} diff --git a/pkg/services/ngalert/api/generated_base_api_testing.go b/pkg/services/ngalert/api/generated_base_api_testing.go index e90d92de071..72a49b463d0 100644 --- a/pkg/services/ngalert/api/generated_base_api_testing.go +++ b/pkg/services/ngalert/api/generated_base_api_testing.go @@ -23,7 +23,7 @@ type TestingApiService interface { func (api *API) RegisterTestingApiEndpoints(srv TestingApiService) { 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/rule/test"), binding.Bind(apimodels.TestRulePayload{}), routing.Wrap(srv.RouteTestRuleConfig)) + group.Post(toMacaronPath("/api/v1/receiver/test/{Recipient}"), binding.Bind(apimodels.ExtendedReceiver{}), routing.Wrap(srv.RouteTestReceiverConfig)) + group.Post(toMacaronPath("/api/v1/rule/test/{Recipient}"), binding.Bind(apimodels.TestRulePayload{}), routing.Wrap(srv.RouteTestRuleConfig)) }, middleware.ReqSignedIn) } diff --git a/pkg/services/ngalert/api/legacy_api_routes.go b/pkg/services/ngalert/api/legacy_api_routes.go index f6fab3f3c99..e00bf165eda 100644 --- a/pkg/services/ngalert/api/legacy_api_routes.go +++ b/pkg/services/ngalert/api/legacy_api_routes.go @@ -24,31 +24,7 @@ func (api *API) listAlertInstancesEndpoint(c *models.ReqContext) response.Respon // conditionEvalEndpoint handles POST /api/alert-definitions/eval. func (api *API) conditionEvalEndpoint(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand) response.Response { - evalCond := ngmodels.Condition{ - 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}, - }) + return conditionEval(c, cmd, api.DatasourceCache, api.DataService, api.Cfg) } // 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) } - 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) } @@ -118,7 +94,8 @@ func (api *API) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels OrgID: c.SignedInUser.OrgId, 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) } @@ -138,7 +115,8 @@ func (api *API) createAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels OrgID: c.SignedInUser.OrgId, 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) } @@ -220,43 +198,6 @@ func (api *API) LoadAlertCondition(alertDefinitionUID string, orgID int64) (*ngm }, 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) { uid := c.ParamsEscape(":alertDefinitionUID") diff --git a/pkg/services/ngalert/api/legacy_trans_dev.go b/pkg/services/ngalert/api/legacy_trans_dev.go index 9e7a72a1b91..d0a113b07ca 100644 --- a/pkg/services/ngalert/api/legacy_trans_dev.go +++ b/pkg/services/ngalert/api/legacy_trans_dev.go @@ -28,7 +28,8 @@ func (api *API) conditionEvalOldEndpoint(c *models.ReqContext) response.Response if err != nil { 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) } //now := cmd.Now @@ -70,7 +71,8 @@ func (api *API) conditionEvalOldEndpointByID(c *models.ReqContext) response.Resp if err != nil { 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) } //now := cmd.Now diff --git a/pkg/services/ngalert/api/test-data/test.http b/pkg/services/ngalert/api/test-data/test.http new file mode 100644 index 00000000000..9476cc0f146 --- /dev/null +++ b/pkg/services/ngalert/api/test-data/test.http @@ -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" +} \ No newline at end of file diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index bf5e0c9a56d..79e650115cf 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -12,10 +12,16 @@ import ( "strings" 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/models" "github.com/grafana/grafana/pkg/services/datasourceproxy" "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/yaml.v3" ) @@ -150,3 +156,68 @@ func jsonExtractor(v interface{}) func([]byte) (interface{}, error) { func messageExtractor(b []byte) (interface{}, error) { 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}, + }) +} diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 4b2d2bccbdd..84f4001119a 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -165,7 +165,7 @@ func execute(ctx AlertExecCtx, c *models.Condition, now time.Time, dataService * } if len(result.Results) == 0 { - err = fmt.Errorf("no GEL results") + err = fmt.Errorf("no transformation results") result.Error = err return &result, err } diff --git a/pkg/services/ngalert/models/eval_condition.go b/pkg/services/ngalert/models/eval_condition.go new file mode 100644 index 00000000000..05c29306c4b --- /dev/null +++ b/pkg/services/ngalert/models/eval_condition.go @@ -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 +} diff --git a/pkg/services/ngalert/models/legacy.go b/pkg/services/ngalert/models/legacy.go index 75dd686b5a0..7f809af393b 100644 --- a/pkg/services/ngalert/models/legacy.go +++ b/pkg/services/ngalert/models/legacy.go @@ -131,11 +131,3 @@ type UpdateAlertDefinitionPausedCommand struct { 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"` -}