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
"net/http"
"net/url"
"strconv"
2022-12-14 22:44:14 +08:00
"time"
2021-04-14 01:58:34 +08:00
2023-06-09 06:59:54 +08:00
"github.com/benbjohnson/clock"
"github.com/grafana/alerting/models"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
2022-04-02 08:00:23 +08:00
2023-10-09 16:40:19 +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"
2023-08-16 15:04:18 +08:00
"github.com/grafana/grafana/pkg/infra/tracing"
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"
2023-06-09 06:59:54 +08:00
"github.com/grafana/grafana/pkg/services/folder"
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"
2023-06-09 06:59:54 +08:00
"github.com/grafana/grafana/pkg/services/ngalert/state"
2023-11-29 08:44:28 +08:00
"github.com/grafana/grafana/pkg/services/ngalert/store"
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
2023-11-16 00:54:54 +08:00
authz RuleAccessControlService
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
2023-06-09 06:59:54 +08:00
appUrl * url . URL
2023-08-16 15:04:18 +08:00
tracer tracing . Tracer
2021-04-14 01:58:34 +08:00
}
2023-06-09 06:59:54 +08:00
// RouteTestGrafanaRuleConfig returns a list of potential alerts for a given rule configuration. This is intended to be
// as true as possible to what would be generated by the ruler except that the resulting alerts are not filtered to
// only Resolved / Firing and ready to send.
func ( srv TestingApiSrv ) RouteTestGrafanaRuleConfig ( c * contextmodel . ReqContext , body apimodels . PostableExtendedRuleNodeExtended ) response . Response {
rule , err := validateRuleNode (
& body . Rule ,
body . RuleGroup ,
srv . cfg . BaseInterval ,
2023-10-09 16:40:19 +08:00
c . SignedInUser . GetOrgID ( ) ,
2023-06-09 06:59:54 +08:00
& folder . Folder {
2023-10-09 16:40:19 +08:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2023-06-09 06:59:54 +08:00
UID : body . NamespaceUID ,
Title : body . NamespaceTitle ,
} ,
srv . cfg ,
)
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "" )
2021-04-14 01:58:34 +08:00
}
2022-04-02 08:00:23 +08:00
2023-12-02 07:42:11 +08:00
if err := srv . authz . AuthorizeAccessToRuleGroup ( c . Req . Context ( ) , c . SignedInUser , ngmodels . RulesGroup { rule } ) ; err != nil {
return response . ErrOrFallback ( http . StatusInternalServerError , "failed to authorize access to rule group" , err )
2022-04-02 08:00:23 +08:00
}
2023-11-29 08:44:28 +08:00
if _ , err := store . OptimizeAlertQueries ( rule . Data ) ; err != nil {
return ErrResp ( http . StatusInternalServerError , err , "Failed to optimize query" )
}
2023-06-09 06:59:54 +08:00
evaluator , err := srv . evaluator . Create ( eval . NewContext ( c . Req . Context ( ) , c . SignedInUser ) , rule . GetEvalCondition ( ) )
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "Failed to build evaluator for queries and expressions" )
2022-04-02 08:00:23 +08:00
}
2023-06-09 06:59:54 +08:00
now := time . Now ( )
results , err := evaluator . Evaluate ( c . Req . Context ( ) , now )
2022-11-02 22:13:39 +08:00
if err != nil {
2023-06-09 06:59:54 +08:00
return ErrResp ( http . StatusInternalServerError , err , "Failed to evaluate queries" )
2022-04-02 08:00:23 +08:00
}
2023-06-09 06:59:54 +08:00
cfg := state . ManagerCfg {
2023-06-23 18:36:07 +08:00
Metrics : nil ,
ExternalURL : srv . appUrl ,
InstanceStore : nil ,
Images : & backtesting . NoopImageService { } ,
Clock : clock . New ( ) ,
Historian : nil ,
MaxStateSaveConcurrency : 1 ,
2023-08-16 15:04:18 +08:00
Tracer : srv . tracer ,
2023-09-20 21:07:02 +08:00
Log : log . New ( "ngalert.state.manager" ) ,
2022-04-02 08:00:23 +08:00
}
2023-06-09 06:59:54 +08:00
manager := state . NewManager ( cfg )
includeFolder := ! srv . cfg . ReservedLabels . IsReservedLabelDisabled ( models . FolderTitleLabel )
transitions := manager . ProcessEvalResults (
c . Req . Context ( ) ,
now ,
rule ,
results ,
state . GetRuleExtraLabels ( rule , body . NamespaceTitle , includeFolder ) ,
)
2022-04-02 08:00:23 +08:00
2023-06-09 06:59:54 +08:00
alerts := make ( [ ] * amv2 . PostableAlert , 0 , len ( transitions ) )
for _ , alertState := range transitions {
2024-01-10 03:47:19 +08:00
alerts = append ( alerts , state . StateToPostableAlert ( alertState , srv . appUrl ) )
2022-11-02 22:13:39 +08:00
}
2022-04-02 08:00:23 +08:00
2023-06-09 06:59:54 +08:00
return response . JSON ( http . StatusOK , alerts )
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 )
2023-12-02 07:42:11 +08:00
if err := srv . authz . AuthorizeDatasourceAccessForRule ( c . Req . Context ( ) , c . SignedInUser , & ngmodels . AlertRule { Data : queries } ) ; err != nil {
return response . ErrOrFallback ( http . StatusInternalServerError , "failed to authorize access to data sources" , err )
2022-04-02 08:00:23 +08:00
}
2022-11-02 22:13:39 +08:00
cond := ngmodels . Condition {
2023-12-07 00:28:43 +08:00
Condition : cmd . Condition ,
2023-03-27 23:55:13 +08:00
Data : queries ,
2022-11-02 22:13:39 +08:00
}
2023-12-07 00:28:43 +08:00
if cond . Condition == "" && len ( cond . Data ) > 0 {
cond . Condition = cond . Data [ len ( cond . Data ) - 1 ] . RefID
2022-11-02 22:13:39 +08:00
}
2023-11-29 08:44:28 +08:00
_ , err := store . OptimizeAlertQueries ( cond . Data )
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "Failed to optimize query" )
}
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 {
2023-11-15 04:50:27 +08:00
if ! srv . featureManager . IsEnabled ( c . Req . Context ( ) , featuremgmt . FlagAlertingBacktesting ) {
2022-12-14 22:44:14 +08:00
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 )
2023-12-02 07:42:11 +08:00
if err := srv . authz . AuthorizeAccessToRuleGroup ( c . Req . Context ( ) , c . SignedInUser , ngmodels . RulesGroup { & ngmodels . AlertRule { Data : queries } } ) ; err != nil {
return errorToResponse ( err )
2022-12-14 22:44:14 +08:00
}
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 ( ) ,
2023-10-09 16:40:19 +08:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2022-12-14 22:44:14 +08:00
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 )
}