2021-04-13 12:58:34 -05:00
package api
import (
2024-02-06 16:12:13 -06:00
"context"
2022-12-14 08:44:14 -06:00
"errors"
2021-04-13 12:58:34 -05:00
"net/http"
"net/url"
"strconv"
2022-12-14 08:44:14 -06:00
"time"
2021-04-13 12:58:34 -05:00
2023-06-08 17:59:54 -05:00
"github.com/benbjohnson/clock"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
2022-04-01 19:00:23 -05:00
2024-01-10 13:40:00 -06:00
"github.com/grafana/alerting/models"
"github.com/grafana/grafana-plugin-sdk-go/backend"
2023-10-09 03:40:19 -05:00
"github.com/grafana/grafana-plugin-sdk-go/data"
2021-04-13 12:58:34 -05:00
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
2023-08-16 02:04:18 -05:00
"github.com/grafana/grafana/pkg/infra/tracing"
2024-02-06 16:12:13 -06:00
"github.com/grafana/grafana/pkg/services/auth/identity"
2023-01-27 01:50:36 -06:00
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
2024-02-06 16:12:13 -06:00
"github.com/grafana/grafana/pkg/services/dashboards"
2021-04-13 12:58:34 -05:00
"github.com/grafana/grafana/pkg/services/datasources"
2022-12-14 08:44:14 -06:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2023-06-08 17:59:54 -05:00
"github.com/grafana/grafana/pkg/services/folder"
2021-04-19 13:26:04 -05:00
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
2022-12-14 08:44:14 -06:00
"github.com/grafana/grafana/pkg/services/ngalert/backtesting"
2021-04-21 14:44:50 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/eval"
2022-04-01 19:00:23 -05:00
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2023-06-08 17:59:54 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/state"
2023-11-28 18:44:28 -06:00
"github.com/grafana/grafana/pkg/services/ngalert/store"
2022-12-14 08:44:14 -06:00
"github.com/grafana/grafana/pkg/setting"
2022-04-01 19:00:23 -05:00
"github.com/grafana/grafana/pkg/util"
2021-04-13 12:58:34 -05:00
)
2024-02-06 16:12:13 -06:00
type folderService interface {
GetNamespaceByUID ( ctx context . Context , uid string , orgID int64 , user identity . Requester ) ( * folder . Folder , error )
}
2021-04-13 12:58:34 -05:00
type TestingApiSrv struct {
* AlertingProxy
2022-06-27 16:40:44 -05:00
DatasourceCache datasources . CacheService
log log . Logger
2023-11-15 10:54:54 -06:00
authz RuleAccessControlService
2022-11-02 09:13:39 -05:00
evaluator eval . EvaluatorFactory
2022-12-14 08:44:14 -06:00
cfg * setting . UnifiedAlertingSettings
backtesting * backtesting . Engine
featureManager featuremgmt . FeatureToggles
2023-06-08 17:59:54 -05:00
appUrl * url . URL
2023-08-16 02:04:18 -05:00
tracer tracing . Tracer
2024-02-06 16:12:13 -06:00
folderService folderService
2021-04-13 12:58:34 -05:00
}
2023-06-08 17:59:54 -05: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 {
2024-02-06 16:12:13 -06:00
folder , err := srv . folderService . GetNamespaceByUID ( c . Req . Context ( ) , body . NamespaceUID , c . OrgID , c . SignedInUser )
if err != nil {
return toNamespaceErrorResponse ( dashboards . ErrFolderAccessDenied )
}
2023-06-08 17:59:54 -05:00
rule , err := validateRuleNode (
& body . Rule ,
body . RuleGroup ,
srv . cfg . BaseInterval ,
2023-10-09 03:40:19 -05:00
c . SignedInUser . GetOrgID ( ) ,
2024-02-28 14:40:13 -06:00
folder . UID ,
RuleLimitsFromConfig ( srv . cfg ) ,
2023-06-08 17:59:54 -05:00
)
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "" )
2021-04-13 12:58:34 -05:00
}
2022-04-01 19:00:23 -05:00
2024-03-19 21:20:30 -05:00
if err := srv . authz . AuthorizeDatasourceAccessForRule ( c . Req . Context ( ) , c . SignedInUser , rule ) ; err != nil {
2023-12-01 17:42:11 -06:00
return response . ErrOrFallback ( http . StatusInternalServerError , "failed to authorize access to rule group" , err )
2022-04-01 19:00:23 -05:00
}
2024-01-10 14:52:58 -06:00
if srv . featureManager . IsEnabled ( c . Req . Context ( ) , featuremgmt . FlagAlertingQueryOptimization ) {
if _ , err := store . OptimizeAlertQueries ( rule . Data ) ; err != nil {
return ErrResp ( http . StatusInternalServerError , err , "Failed to optimize query" )
}
2023-11-28 18:44:28 -06:00
}
2023-06-08 17:59:54 -05: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-01 19:00:23 -05:00
}
2023-06-08 17:59:54 -05:00
now := time . Now ( )
results , err := evaluator . Evaluate ( c . Req . Context ( ) , now )
2022-11-02 09:13:39 -05:00
if err != nil {
2023-06-08 17:59:54 -05:00
return ErrResp ( http . StatusInternalServerError , err , "Failed to evaluate queries" )
2022-04-01 19:00:23 -05:00
}
2023-06-08 17:59:54 -05:00
cfg := state . ManagerCfg {
2024-01-17 06:33:13 -06:00
Metrics : nil ,
ExternalURL : srv . appUrl ,
InstanceStore : nil ,
Images : & backtesting . NoopImageService { } ,
Clock : clock . New ( ) ,
Historian : nil ,
Tracer : srv . tracer ,
Log : log . New ( "ngalert.state.manager" ) ,
2022-04-01 19:00:23 -05:00
}
2024-01-17 06:33:13 -06:00
manager := state . NewManager ( cfg , state . NewNoopPersister ( ) )
2023-06-08 17:59:54 -05:00
includeFolder := ! srv . cfg . ReservedLabels . IsReservedLabelDisabled ( models . FolderTitleLabel )
transitions := manager . ProcessEvalResults (
c . Req . Context ( ) ,
now ,
rule ,
results ,
2024-02-15 08:45:10 -06:00
state . GetRuleExtraLabels ( log . New ( "testing" ) , rule , folder . Fullpath , includeFolder ) ,
2023-06-08 17:59:54 -05:00
)
2022-04-01 19:00:23 -05:00
2023-06-08 17:59:54 -05:00
alerts := make ( [ ] * amv2 . PostableAlert , 0 , len ( transitions ) )
for _ , alertState := range transitions {
2024-01-09 13:47:19 -06:00
alerts = append ( alerts , state . StateToPostableAlert ( alertState , srv . appUrl ) )
2022-11-02 09:13:39 -05:00
}
2022-04-01 19:00:23 -05:00
2023-06-08 17:59:54 -05:00
return response . JSON ( http . StatusOK , alerts )
2022-02-04 11:42:04 -06:00
}
2021-04-13 12:58:34 -05:00
2023-01-27 01:50:36 -06:00
func ( srv TestingApiSrv ) RouteTestRuleConfig ( c * contextmodel . ReqContext , body apimodels . TestRulePayload , datasourceUID string ) response . Response {
2021-04-13 12:58:34 -05:00
if body . Type ( ) != apimodels . LoTexRulerBackend {
2022-08-02 08:33:59 -05:00
return errorToResponse ( backendTypeDoesNotMatchPayloadTypeError ( apimodels . LoTexRulerBackend , body . Type ( ) . String ( ) ) )
2021-04-13 12:58:34 -05:00
}
2022-08-02 08:33:59 -05:00
ds , err := getDatasourceByUID ( c , srv . DatasourceCache , apimodels . LoTexRulerBackend )
2022-05-17 06:10:20 -05:00
if err != nil {
2022-08-02 08:33:59 -05:00
return errorToResponse ( err )
2022-05-17 06:10:20 -05:00
}
2022-08-02 08:33:59 -05:00
var path string
2022-05-17 06:10:20 -05:00
switch ds . Type {
case "loki" :
path = "loki/api/v1/query"
case "prometheus" :
path = "api/v1/query"
default :
2022-08-02 08:33:59 -05:00
// this should not happen because getDatasourceByUID would not return the data source
return errorToResponse ( unexpectedDatasourceTypeError ( ds . Type , "loki, prometheus" ) )
2021-04-13 12:58:34 -05:00
}
t := timeNow ( )
queryURL , err := url . Parse ( path )
if err != nil {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusInternalServerError , err , "failed to parse url" )
2021-04-13 12:58:34 -05: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 10:54:50 -05:00
instantQueryResultsExtractor ,
2021-04-13 12:58:34 -05:00
nil ,
)
}
2021-04-21 14:44:50 -05:00
2023-01-27 01:50:36 -06:00
func ( srv TestingApiSrv ) RouteEvalQueries ( c * contextmodel . ReqContext , cmd apimodels . EvalQueriesPayload ) response . Response {
2023-03-27 10:55:13 -05:00
queries := AlertQueriesFromApiAlertQueries ( cmd . Data )
2023-12-01 17:42:11 -06: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-01 19:00:23 -05:00
}
2022-11-02 09:13:39 -05:00
cond := ngmodels . Condition {
2023-12-06 10:28:43 -06:00
Condition : cmd . Condition ,
2023-03-27 10:55:13 -05:00
Data : queries ,
2022-11-02 09:13:39 -05:00
}
2023-12-06 10:28:43 -06:00
if cond . Condition == "" && len ( cond . Data ) > 0 {
cond . Condition = cond . Data [ len ( cond . Data ) - 1 ] . RefID
2022-11-02 09:13:39 -05:00
}
2023-11-28 18:44:28 -06:00
2024-01-10 14:52:58 -06:00
var optimizations [ ] store . Optimization
if srv . featureManager . IsEnabled ( c . Req . Context ( ) , featuremgmt . FlagAlertingQueryOptimization ) {
var err error
optimizations , err = store . OptimizeAlertQueries ( cond . Data )
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "Failed to optimize query" )
}
2023-11-28 18:44:28 -06:00
}
2023-04-06 10:02:28 -05:00
evaluator , err := srv . evaluator . Create ( eval . NewContext ( c . Req . Context ( ) , c . SignedInUser ) , cond )
2022-11-02 09:13:39 -05:00
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "Failed to build evaluator for queries and expressions" )
2022-10-19 14:19:43 -05:00
}
2022-11-02 09:13:39 -05:00
now := cmd . Now
if now . IsZero ( ) {
now = timeNow ( )
}
evalResults , err := evaluator . EvaluateRaw ( c . Req . Context ( ) , now )
2021-04-21 14:44:50 -05:00
if err != nil {
2022-11-02 09:13:39 -05:00
return ErrResp ( http . StatusInternalServerError , err , "Failed to evaluate queries and expressions" )
2021-04-21 14:44:50 -05:00
}
2024-01-10 13:40:00 -06:00
addOptimizedQueryWarnings ( evalResults , optimizations )
2021-04-21 14:44:50 -05:00
return response . JSONStreaming ( http . StatusOK , evalResults )
}
2022-12-14 08:44:14 -06:00
2024-01-10 13:40:00 -06:00
// addOptimizedQueryWarnings adds warnings to the query results for any queries that were optimized.
func addOptimizedQueryWarnings ( evalResults * backend . QueryDataResponse , optimizations [ ] store . Optimization ) {
for _ , opt := range optimizations {
if res , ok := evalResults . Responses [ opt . RefID ] ; ok {
if len ( res . Frames ) > 0 {
res . Frames [ 0 ] . AppendNotices ( data . Notice {
Severity : data . NoticeSeverityWarning ,
Text : "Query optimized from Range to Instant type; all uses exclusively require the last datapoint. " +
"Consider modifying your query to Instant type to ensure accuracy." , // Currently this is the only optimization we do.
} )
}
}
}
}
2023-01-27 01:50:36 -06:00
func ( srv TestingApiSrv ) BacktestAlertRule ( c * contextmodel . ReqContext , cmd apimodels . BacktestConfig ) response . Response {
2023-11-14 14:50:27 -06:00
if ! srv . featureManager . IsEnabled ( c . Req . Context ( ) , featuremgmt . FlagAlertingBacktesting ) {
2022-12-14 08:44:14 -06: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" )
}
2024-02-28 14:40:13 -06:00
intervalSeconds , err := validateInterval ( time . Duration ( cmd . Interval ) , srv . cfg . BaseInterval )
2022-12-14 08:44:14 -06:00
if err != nil {
return ErrResp ( 400 , err , "" )
}
2023-03-27 10:55:13 -05:00
queries := AlertQueriesFromApiAlertQueries ( cmd . Data )
2024-03-19 21:20:30 -05:00
if err := srv . authz . AuthorizeDatasourceAccessForRule ( c . Req . Context ( ) , c . SignedInUser , & ngmodels . AlertRule { Data : queries } ) ; err != nil {
2023-12-01 17:42:11 -06:00
return errorToResponse ( err )
2022-12-14 08:44:14 -06: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 03:40:19 -05:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2022-12-14 08:44:14 -06:00
Condition : cmd . Condition ,
2023-03-27 10:55:13 -05:00
Data : queries ,
2022-12-14 08:44:14 -06: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 )
}