2021-04-14 01:58:34 +08:00
package api
import (
2022-12-14 22:44:14 +08:00
"errors"
2021-04-14 01:58:34 +08:00
"fmt"
"net/http"
"net/url"
"strconv"
2022-12-14 22:44:14 +08:00
"time"
2021-04-14 01:58:34 +08:00
2022-04-02 08:00:23 +08:00
"github.com/grafana/grafana-plugin-sdk-go/data"
2021-04-14 01:58:34 +08:00
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
2022-04-02 08:00:23 +08:00
"github.com/grafana/grafana/pkg/services/accesscontrol"
2023-01-27 15:50:36 +08:00
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
2021-04-14 01:58:34 +08:00
"github.com/grafana/grafana/pkg/services/datasources"
2022-12-14 22:44:14 +08:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2021-04-20 02:26:04 +08:00
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
2022-12-14 22:44:14 +08:00
"github.com/grafana/grafana/pkg/services/ngalert/backtesting"
2021-04-22 03:44:50 +08:00
"github.com/grafana/grafana/pkg/services/ngalert/eval"
2022-04-02 08:00:23 +08:00
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2022-12-14 22:44:14 +08:00
"github.com/grafana/grafana/pkg/setting"
2022-04-02 08:00:23 +08:00
"github.com/grafana/grafana/pkg/util"
2021-04-14 01:58:34 +08:00
)
type TestingApiSrv struct {
* AlertingProxy
2022-06-28 05:40:44 +08:00
DatasourceCache datasources . CacheService
log log . Logger
accessControl accesscontrol . AccessControl
2022-11-02 22:13:39 +08:00
evaluator eval . EvaluatorFactory
2022-12-14 22:44:14 +08:00
cfg * setting . UnifiedAlertingSettings
backtesting * backtesting . Engine
featureManager featuremgmt . FeatureToggles
2021-04-14 01:58:34 +08:00
}
2023-01-27 15:50:36 +08:00
func ( srv TestingApiSrv ) RouteTestGrafanaRuleConfig ( c * contextmodel . ReqContext , body apimodels . TestRulePayload ) response . Response {
2022-02-05 01:42:04 +08:00
if body . Type ( ) != apimodels . GrafanaBackend || body . GrafanaManagedCondition == nil {
2022-08-02 21:33:59 +08:00
return errorToResponse ( backendTypeDoesNotMatchPayloadTypeError ( apimodels . GrafanaBackend , body . Type ( ) . String ( ) ) )
2021-04-14 01:58:34 +08:00
}
2022-04-02 08:00:23 +08:00
2023-03-27 23:55:13 +08:00
queries := AlertQueriesFromApiAlertQueries ( body . GrafanaManagedCondition . Data )
if ! authorizeDatasourceAccessForRule ( & ngmodels . AlertRule { Data : queries } , func ( evaluator accesscontrol . Evaluator ) bool {
2023-05-30 21:39:09 +08:00
return accesscontrol . HasAccess ( srv . accessControl , c ) ( evaluator )
2022-04-02 08:00:23 +08:00
} ) {
2022-08-02 21:33:59 +08:00
return errorToResponse ( fmt . Errorf ( "%w to query one or many data sources used by the rule" , ErrAuthorization ) )
2022-04-02 08:00:23 +08:00
}
evalCond := ngmodels . Condition {
Condition : body . GrafanaManagedCondition . Condition ,
2023-03-27 23:55:13 +08:00
Data : queries ,
2022-04-02 08:00:23 +08:00
}
2023-04-06 23:02:28 +08:00
ctx := eval . NewContext ( c . Req . Context ( ) , c . SignedInUser )
2022-04-02 08:00:23 +08:00
2022-11-02 22:13:39 +08:00
conditionEval , err := srv . evaluator . Create ( ctx , evalCond )
if err != nil {
2022-04-02 08:00:23 +08:00
return ErrResp ( http . StatusBadRequest , err , "invalid condition" )
}
2022-11-02 22:13:39 +08:00
now := body . GrafanaManagedCondition . Now
if now . IsZero ( ) {
now = timeNow ( )
2022-04-02 08:00:23 +08:00
}
2022-11-02 22:13:39 +08:00
evalResults , err := conditionEval . Evaluate ( c . Req . Context ( ) , now )
if err != nil {
return ErrResp ( 500 , err , "Failed to evaluate the rule" )
}
2022-04-02 08:00:23 +08:00
frame := evalResults . AsDataFrame ( )
return response . JSONStreaming ( http . StatusOK , util . DynMap {
"instances" : [ ] * data . Frame { & frame } ,
} )
2022-02-05 01:42:04 +08:00
}
2021-04-14 01:58:34 +08:00
2023-01-27 15:50:36 +08:00
func ( srv TestingApiSrv ) RouteTestRuleConfig ( c * contextmodel . ReqContext , body apimodels . TestRulePayload , datasourceUID string ) response . Response {
2021-04-14 01:58:34 +08:00
if body . Type ( ) != apimodels . LoTexRulerBackend {
2022-08-02 21:33:59 +08:00
return errorToResponse ( backendTypeDoesNotMatchPayloadTypeError ( apimodels . LoTexRulerBackend , body . Type ( ) . String ( ) ) )
2021-04-14 01:58:34 +08:00
}
2022-08-02 21:33:59 +08:00
ds , err := getDatasourceByUID ( c , srv . DatasourceCache , apimodels . LoTexRulerBackend )
2022-05-17 19:10:20 +08:00
if err != nil {
2022-08-02 21:33:59 +08:00
return errorToResponse ( err )
2022-05-17 19:10:20 +08:00
}
2022-08-02 21:33:59 +08:00
var path string
2022-05-17 19:10:20 +08:00
switch ds . Type {
case "loki" :
path = "loki/api/v1/query"
case "prometheus" :
path = "api/v1/query"
default :
2022-08-02 21:33:59 +08:00
// this should not happen because getDatasourceByUID would not return the data source
return errorToResponse ( unexpectedDatasourceTypeError ( ds . Type , "loki, prometheus" ) )
2021-04-14 01:58:34 +08:00
}
t := timeNow ( )
queryURL , err := url . Parse ( path )
if err != nil {
2021-05-28 23:55:03 +08:00
return ErrResp ( http . StatusInternalServerError , err , "failed to parse url" )
2021-04-14 01:58:34 +08:00
}
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 ,
2021-05-25 23:54:50 +08:00
instantQueryResultsExtractor ,
2021-04-14 01:58:34 +08:00
nil ,
)
}
2021-04-22 03:44:50 +08:00
2023-01-27 15:50:36 +08:00
func ( srv TestingApiSrv ) RouteEvalQueries ( c * contextmodel . ReqContext , cmd apimodels . EvalQueriesPayload ) response . Response {
2023-03-27 23:55:13 +08:00
queries := AlertQueriesFromApiAlertQueries ( cmd . Data )
if ! authorizeDatasourceAccessForRule ( & ngmodels . AlertRule { Data : queries } , func ( evaluator accesscontrol . Evaluator ) bool {
2023-05-30 21:39:09 +08:00
return accesscontrol . HasAccess ( srv . accessControl , c ) ( evaluator )
2022-04-02 08:00:23 +08:00
} ) {
return ErrResp ( http . StatusUnauthorized , fmt . Errorf ( "%w to query one or many data sources used by the rule" , ErrAuthorization ) , "" )
}
2022-11-02 22:13:39 +08:00
cond := ngmodels . Condition {
Condition : "" ,
2023-03-27 23:55:13 +08:00
Data : queries ,
2022-11-02 22:13:39 +08:00
}
if len ( cmd . Data ) > 0 {
cond . Condition = cmd . Data [ 0 ] . RefID
}
2023-04-06 23:02:28 +08:00
evaluator , err := srv . evaluator . Create ( eval . NewContext ( c . Req . Context ( ) , c . SignedInUser ) , cond )
2022-11-02 22:13:39 +08:00
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "Failed to build evaluator for queries and expressions" )
2022-10-20 03:19:43 +08:00
}
2022-11-02 22:13:39 +08:00
now := cmd . Now
if now . IsZero ( ) {
now = timeNow ( )
}
evalResults , err := evaluator . EvaluateRaw ( c . Req . Context ( ) , now )
2021-04-22 03:44:50 +08:00
if err != nil {
2022-11-02 22:13:39 +08:00
return ErrResp ( http . StatusInternalServerError , err , "Failed to evaluate queries and expressions" )
2021-04-22 03:44:50 +08:00
}
return response . JSONStreaming ( http . StatusOK , evalResults )
}
2022-12-14 22:44:14 +08:00
2023-01-27 15:50:36 +08:00
func ( srv TestingApiSrv ) BacktestAlertRule ( c * contextmodel . ReqContext , cmd apimodels . BacktestConfig ) response . Response {
2022-12-14 22:44:14 +08:00
if ! srv . featureManager . IsEnabled ( featuremgmt . FlagAlertingBacktesting ) {
return ErrResp ( http . StatusNotFound , nil , "Backgtesting API is not enabled" )
}
if cmd . From . After ( cmd . To ) {
return ErrResp ( 400 , nil , "From cannot be greater than To" )
}
noDataState , err := ngmodels . NoDataStateFromString ( string ( cmd . NoDataState ) )
if err != nil {
return ErrResp ( 400 , err , "" )
}
forInterval := time . Duration ( cmd . For )
if forInterval < 0 {
return ErrResp ( 400 , nil , "Bad For interval" )
}
intervalSeconds , err := validateInterval ( srv . cfg , time . Duration ( cmd . Interval ) )
if err != nil {
return ErrResp ( 400 , err , "" )
}
2023-03-27 23:55:13 +08:00
queries := AlertQueriesFromApiAlertQueries ( cmd . Data )
if ! authorizeDatasourceAccessForRule ( & ngmodels . AlertRule { Data : queries } , func ( evaluator accesscontrol . Evaluator ) bool {
2023-05-30 21:39:09 +08:00
return accesscontrol . HasAccess ( srv . accessControl , c ) ( evaluator )
2022-12-14 22:44:14 +08:00
} ) {
return errorToResponse ( fmt . Errorf ( "%w to query one or many data sources used by the rule" , ErrAuthorization ) )
}
rule := & ngmodels . AlertRule {
// ID: 0,
// Updated: time.Time{},
// Version: 0,
// NamespaceUID: "",
// DashboardUID: nil,
// PanelID: nil,
// RuleGroup: "",
// RuleGroupIndex: 0,
// ExecErrState: "",
Title : cmd . Title ,
// prefix backtesting- is to distinguish between executions of regular rule and backtesting in logs (like expression engine, evaluator, state manager etc)
UID : "backtesting-" + util . GenerateShortUID ( ) ,
OrgID : c . OrgID ,
Condition : cmd . Condition ,
2023-03-27 23:55:13 +08:00
Data : queries ,
2022-12-14 22:44:14 +08:00
IntervalSeconds : intervalSeconds ,
NoDataState : noDataState ,
For : forInterval ,
Annotations : cmd . Annotations ,
Labels : cmd . Labels ,
}
result , err := srv . backtesting . Test ( c . Req . Context ( ) , c . SignedInUser , rule , cmd . From , cmd . To )
if err != nil {
if errors . Is ( err , backtesting . ErrInvalidInputData ) {
return ErrResp ( 400 , err , "Failed to evaluate" )
}
return ErrResp ( 500 , err , "Failed to evaluate" )
}
body , err := data . FrameToJSON ( result , data . IncludeAll )
if err != nil {
return ErrResp ( 500 , err , "Failed to convert frame to JSON" )
}
return response . JSON ( http . StatusOK , body )
}