2021-04-14 01:58:34 +08:00
package api
import (
2024-02-07 06:12:13 +08:00
"context"
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"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
2022-04-02 08:00:23 +08:00
2024-01-11 03:40:00 +08:00
"github.com/grafana/alerting/models"
"github.com/grafana/grafana-plugin-sdk-go/backend"
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"
2024-06-13 12:11:35 +08:00
"github.com/grafana/grafana/pkg/apimachinery/identity"
2021-04-14 01:58:34 +08:00
"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"
2024-02-07 06:12:13 +08:00
"github.com/grafana/grafana/pkg/services/dashboards"
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"
2025-03-13 16:28:35 +08:00
. "github.com/grafana/grafana/pkg/services/ngalert/api/compat"
2021-04-20 02:26:04 +08:00
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
2025-03-13 16:28:35 +08:00
apivalidation "github.com/grafana/grafana/pkg/services/ngalert/api/validation"
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
)
2024-02-07 06:12:13 +08:00
type folderService interface {
GetNamespaceByUID ( ctx context . Context , uid string , orgID int64 , user identity . Requester ) ( * folder . Folder , error )
}
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
2024-02-07 06:12:13 +08:00
folderService folderService
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 {
2024-02-07 06:12:13 +08:00
folder , err := srv . folderService . GetNamespaceByUID ( c . Req . Context ( ) , body . NamespaceUID , c . OrgID , c . SignedInUser )
if err != nil {
return toNamespaceErrorResponse ( dashboards . ErrFolderAccessDenied )
}
2025-03-13 16:28:35 +08:00
rule , err := apivalidation . ValidateRuleNode (
2023-06-09 06:59:54 +08:00
& body . Rule ,
body . RuleGroup ,
srv . cfg . BaseInterval ,
2025-04-10 20:42:23 +08:00
c . GetOrgID ( ) ,
2024-02-29 04:40:13 +08:00
folder . UID ,
2025-03-13 16:28:35 +08:00
apivalidation . RuleLimitsFromConfig ( srv . cfg , srv . featureManager ) ,
2023-06-09 06:59:54 +08:00
)
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "" )
2021-04-14 01:58:34 +08:00
}
2022-04-02 08:00:23 +08:00
2024-03-20 10:20:30 +08:00
if err := srv . authz . AuthorizeDatasourceAccessForRule ( c . Req . Context ( ) , c . SignedInUser , rule ) ; err != nil {
2023-12-02 07:42:11 +08:00
return response . ErrOrFallback ( http . StatusInternalServerError , "failed to authorize access to rule group" , err )
2022-04-02 08:00:23 +08:00
}
2024-01-11 04:52:58 +08: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-29 08:44:28 +08:00
}
2024-07-18 03:55:12 +08:00
evaluator , err := srv . evaluator . Create ( eval . NewContext ( c . Req . Context ( ) , c . SignedInUser ) , rule . GetEvalCondition ( ) . WithSource ( "preview" ) )
2023-06-09 06:59:54 +08:00
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 {
2024-01-17 20:33:13 +08: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-02 08:00:23 +08:00
}
2024-01-17 20:33:13 +08:00
manager := state . NewManager ( cfg , state . NewNoopPersister ( ) )
2023-06-09 06:59:54 +08:00
includeFolder := ! srv . cfg . ReservedLabels . IsReservedLabelDisabled ( models . FolderTitleLabel )
transitions := manager . ProcessEvalResults (
c . Req . Context ( ) ,
now ,
rule ,
results ,
2025-10-02 03:21:33 +08:00
state . GetRuleExtraLabels ( log . New ( "testing" ) , rule , folder . Fullpath , includeFolder , srv . featureManager ) ,
2024-06-26 01:01:26 +08:00
nil ,
2023-06-09 06:59:54 +08:00
)
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 {
2025-04-22 19:16:38 +08:00
alerts = append ( alerts , state . StateToPostableAlert ( alertState , srv . appUrl , srv . featureManager ) )
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
2024-01-11 04:52:58 +08: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-29 08:44:28 +08:00
}
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
}
2024-01-11 03:40:00 +08:00
addOptimizedQueryWarnings ( evalResults , optimizations )
2021-04-22 03:44:50 +08:00
return response . JSONStreaming ( http . StatusOK , evalResults )
}
2022-12-14 22:44:14 +08:00
2024-01-11 03:40:00 +08: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 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" )
}
2025-03-13 16:28:35 +08:00
intervalSeconds , err := apivalidation . ValidateInterval ( time . Duration ( cmd . Interval ) , srv . cfg . BaseInterval )
2022-12-14 22:44:14 +08:00
if err != nil {
return ErrResp ( 400 , err , "" )
}
2023-03-27 23:55:13 +08:00
queries := AlertQueriesFromApiAlertQueries ( cmd . Data )
2024-03-20 10:20:30 +08:00
if err := srv . authz . AuthorizeDatasourceAccessForRule ( c . Req . Context ( ) , c . SignedInUser , & ngmodels . AlertRule { Data : queries } ) ; err != nil {
2023-12-02 07:42:11 +08:00
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 ( ) ,
2025-04-10 20:42:23 +08:00
OrgID : c . 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 )
}