[Alerting]: Add alerting endpoint for Query Evaluation (#33174)

* [Alerting]: Add alerting endpoint for Query Evaluation

* Fix passing down now parameter

* Add validations and test

* Fix eval queries and expressions test

* Add eval tests
This commit is contained in:
Sofia Papagiannaki
2021-04-21 22:44:50 +03:00
committed by GitHub
parent ed3f5e6ca3
commit b2288f7ef9
10 changed files with 4315 additions and 3765 deletions
+19
View File
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util"
@@ -77,3 +78,21 @@ func (srv TestingApiSrv) RouteTestRuleConfig(c *models.ReqContext, body apimodel
nil,
)
}
func (srv TestingApiSrv) RouteEvalQueries(c *models.ReqContext, cmd apimodels.EvalQueriesPayload) response.Response {
now := cmd.Now
if now.IsZero() {
now = timeNow()
}
if err := validateQueriesAndExpressions(cmd.Data, c.SignedInUser, c.SkipCache, srv.DatasourceCache); err != nil {
return response.Error(http.StatusBadRequest, "invalid queries or expressions", err)
}
evaluator := eval.Evaluator{Cfg: srv.Cfg}
evalResults, err := evaluator.QueriesAndExpressionsEval(c.SignedInUser.OrgId, cmd.Data, now, srv.DataService)
if err != nil {
return response.Error(http.StatusBadRequest, "Failed to evaluate queries and expressions", err)
}
return response.JSONStreaming(http.StatusOK, evalResults)
}
@@ -18,12 +18,14 @@ import (
)
type TestingApiService interface {
RouteEvalQueries(*models.ReqContext, apimodels.EvalQueriesPayload) response.Response
RouteTestReceiverConfig(*models.ReqContext, apimodels.ExtendedReceiver) response.Response
RouteTestRuleConfig(*models.ReqContext, apimodels.TestRulePayload) response.Response
}
func (api *API) RegisterTestingApiEndpoints(srv TestingApiService) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Post(toMacaronPath("/api/v1/eval"), binding.Bind(apimodels.EvalQueriesPayload{}), routing.Wrap(srv.RouteEvalQueries))
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)
@@ -25,6 +25,52 @@ content-type: application/json
}
}
###
POST http://admin:admin@localhost:3000/api/v1/eval
content-type: application/json
{
"data": [
{
"refId": "A",
"queryType": "",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"model": {
"datasourceUid": "000000004",
"intervalMs": 1000,
"maxDataPoints": 100,
"orgId": 0,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0"
}
},
{
"refId": "B",
"queryType": "",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"model": {
"datasourceUid": "-100",
"expression": "$A",
"intervalMs": 2000,
"maxDataPoints": 200,
"orgId": 0,
"reducer": "mean",
"refId": "B",
"type": "reduce"
}
}
],
"now": "2021-04-11T14:38:14Z"
}
###
POST http://admin:admin@localhost:3000/api/v1/rule/test/{{lokiDatasourceID}}
content-type: application/json
@@ -3,6 +3,9 @@ package definitions
import (
"encoding/json"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/prometheus/alertmanager/config"
@@ -37,6 +40,19 @@ import (
// Responses:
// 200: TestRuleResponse
// swagger:route Post /api/v1/eval testing RouteEvalQueries
//
// Test rule
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Responses:
// 200: EvalQueriesResponse
// swagger:parameters RouteTestReceiverConfig
type TestReceiverRequest struct {
// in:body
@@ -57,6 +73,18 @@ type TestRulePayload struct {
GrafanaManagedCondition *models.EvalAlertConditionCommand `json:"grafana_condition,omitempty"`
}
// swagger:parameters RouteEvalQueries
type EvalQueriesRequest struct {
// in:body
Body EvalQueriesPayload
}
// swagger:model
type EvalQueriesPayload struct {
Data []models.AlertQuery `json:"data"`
Now time.Time `json:"now"`
}
func (p *TestRulePayload) UnmarshalJSON(b []byte) error {
type plain TestRulePayload
if err := json.Unmarshal(b, (*plain)(p)); err != nil {
@@ -96,6 +124,9 @@ type TestRuleResponse struct {
GrafanaAlertInstances AlertInstancesResponse `json:"grafana_alert_instances"`
}
// swagger:model
type EvalQueriesResponse = backend.QueryDataResponse
// swagger:model
type AlertInstancesResponse struct {
// Instances is an array of arrow encoded dataframes
File diff suppressed because it is too large Load Diff
+56 -18
View File
@@ -1,14 +1,4 @@
{
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"schemes": [
"http",
"https"
],
"swagger": "2.0",
"info": {
"description": "Package definitions includes the types required for generating or consuming an OpenAPI\nspec for the Unified Alerting API.\nDocumentation of the API.",
@@ -717,6 +707,38 @@
}
}
},
"/api/v1/eval": {
"post": {
"description": "Test rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"testing"
],
"operationId": "RouteEvalQueries",
"parameters": [
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/EvalQueriesPayload"
}
}
],
"responses": {
"200": {
"description": "EvalQueriesResponse",
"schema": {
"$ref": "#/definitions/EvalQueriesResponse"
}
}
}
}
},
"/api/v1/receiver/test/{Recipient}": {
"post": {
"description": "Test receiver",
@@ -1332,6 +1354,27 @@
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models"
},
"EvalQueriesPayload": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertQuery"
},
"x-go-name": "Data"
},
"now": {
"type": "string",
"format": "date-time",
"x-go-name": "Now"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"EvalQueriesResponse": {
"$ref": "#/definitions/EvalQueriesResponse"
},
"ExtendedReceiver": {
"type": "object",
"properties": {
@@ -1369,7 +1412,7 @@
"$ref": "#/definitions/ResponseDetails"
},
"GettableAlert": {
"$ref": "#/definitions/GettableAlert"
"$ref": "#/definitions/gettableAlert"
},
"GettableAlerts": {
"$ref": "#/definitions/GettableAlerts"
@@ -1638,7 +1681,7 @@
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"GettableSilence": {
"$ref": "#/definitions/gettableSilence"
"$ref": "#/definitions/GettableSilence"
},
"GettableSilences": {
"$ref": "#/definitions/GettableSilences"
@@ -3204,7 +3247,7 @@
"description": "alerts",
"type": "array",
"items": {
"$ref": "#/definitions/gettableAlert"
"$ref": "#/definitions/GettableAlert"
},
"x-go-name": "Alerts"
},
@@ -3775,10 +3818,5 @@
"x-go-name": "VersionInfo",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
}
},
"securityDefinitions": {
"basic": {
"type": "basic"
}
}
}
+30 -3
View File
@@ -194,6 +194,33 @@ func validateCondition(c ngmodels.Condition, user *models.SignedInUser, skipCach
return nil
}
func validateQueriesAndExpressions(data []ngmodels.AlertQuery, user *models.SignedInUser, skipCache bool, datasourceCache datasources.CacheService) error {
if len(data) == 0 {
return nil
}
for _, query := range data {
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)
}
}
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,
@@ -210,14 +237,14 @@ func conditionEval(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand,
}
evaluator := eval.Evaluator{Cfg: cfg}
evalResults, err := evaluator.ConditionEval(&evalCond, timeNow(), dataService)
evalResults, err := evaluator.ConditionEval(&evalCond, now, dataService)
if err != nil {
return response.Error(400, "Failed to evaluate conditions", err)
return response.Error(http.StatusBadRequest, "Failed to evaluate conditions", err)
}
frame := evalResults.AsDataFrame()
return response.JSONStreaming(200, util.DynMap{
return response.JSONStreaming(http.StatusOK, util.DynMap{
"instances": []*data.Frame{&frame},
})
}