2021-04-05 15:05:39 -07:00
package api
import (
2021-04-27 16:15:00 -04:00
"encoding/json"
2021-10-04 16:33:55 +01:00
"errors"
2021-04-13 14:38:09 -07:00
"fmt"
2021-04-05 15:05:39 -07:00
"net/http"
2022-04-05 19:36:42 +01:00
"sort"
2021-10-04 16:33:55 +01:00
"strconv"
"strings"
2021-04-13 14:38:09 -07:00
"time"
2021-04-05 15:05:39 -07:00
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
2022-04-11 17:37:44 -04:00
"github.com/grafana/grafana/pkg/services/accesscontrol"
2021-04-19 14:26:04 -04:00
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
2022-03-16 16:04:19 +00:00
"github.com/grafana/grafana/pkg/services/ngalert/eval"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2021-04-05 15:05:39 -07:00
"github.com/grafana/grafana/pkg/services/ngalert/state"
2022-03-16 16:04:19 +00:00
"github.com/grafana/grafana/pkg/services/ngalert/store"
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
2021-04-05 15:05:39 -07:00
)
type PrometheusSrv struct {
2021-04-23 12:32:25 -07:00
log log . Logger
2022-03-09 18:20:29 +00:00
manager state . AlertInstanceManager
2021-04-23 12:32:25 -07:00
store store . RuleStore
2022-04-11 17:37:44 -04:00
ac accesscontrol . AccessControl
2021-04-05 15:05:39 -07:00
}
2022-03-16 16:04:19 +00:00
const queryIncludeInternalLabels = "includeInternalLabels"
2021-04-05 15:05:39 -07:00
func ( srv PrometheusSrv ) RouteGetAlertStatuses ( c * models . ReqContext ) response . Response {
alertResponse := apimodels . AlertResponse {
DiscoveryBase : apimodels . DiscoveryBase {
Status : "success" ,
},
Data : apimodels . AlertDiscovery {
Alerts : [] * apimodels . Alert {},
},
}
2022-03-16 16:04:19 +00:00
var labelOptions [] ngmodels . LabelOption
if ! c . QueryBoolWithDefault ( queryIncludeInternalLabels , false ) {
labelOptions = append ( labelOptions , ngmodels . WithoutInternalLabels ())
}
2022-08-11 13:28:55 +02:00
for _ , alertState := range srv . manager . GetAll ( c . OrgID ) {
2021-04-05 15:05:39 -07:00
startsAt := alertState . StartsAt
2021-05-18 09:12:39 -04:00
valString := ""
2022-04-05 19:36:42 +01:00
if alertState . State == eval . Alerting || alertState . State == eval . Pending {
valString = formatValues ( alertState )
2021-05-18 09:12:39 -04:00
}
2022-03-09 18:20:29 +00:00
2021-04-05 15:05:39 -07:00
alertResponse . Data . Alerts = append ( alertResponse . Data . Alerts , & apimodels . Alert {
2022-03-16 16:04:19 +00:00
Labels : alertState . GetLabels ( labelOptions ... ),
2022-03-09 18:20:29 +00:00
Annotations : alertState . Annotations ,
2022-05-23 16:49:49 +08:00
// TODO: or should we make this two fields? Using one field lets the
// frontend use the same logic for parsing text on annotations and this.
State : state . InstanceStateAndReason {
State : alertState . State ,
Reason : alertState . StateReason ,
}. String (),
ActiveAt : & startsAt ,
Value : valString ,
2021-04-05 15:05:39 -07:00
})
}
2022-03-09 18:20:29 +00:00
2021-04-05 15:05:39 -07:00
return response . JSON ( http . StatusOK , alertResponse )
}
2022-04-05 19:36:42 +01:00
func formatValues ( alertState * state . State ) string {
var fv string
values := alertState . GetLastEvaluationValuesForCondition ()
switch len ( values ) {
case 0 :
fv = alertState . LastEvaluationString
case 1 :
for _ , v := range values {
fv = strconv . FormatFloat ( v , 'e' , - 1 , 64 )
break
}
default :
vs := make ([] string , 0 , len ( values ))
for k , v := range values {
vs = append ( vs , fmt . Sprintf ( "%s: %s" , k , strconv . FormatFloat ( v , 'e' , - 1 , 64 )))
}
// Ensure we have a consistent natural ordering after formatting e.g. A0, A1, A10, A11, A3, etc.
sort . Strings ( vs )
fv = strings . Join ( vs , ", " )
}
return fv
}
2021-10-04 16:33:55 +01:00
func getPanelIDFromRequest ( r * http . Request ) ( int64 , error ) {
if s := strings . TrimSpace ( r . URL . Query (). Get ( "panel_id" )); s != "" {
return strconv . ParseInt ( s , 10 , 64 )
}
return 0 , nil
}
2021-04-05 15:05:39 -07:00
func ( srv PrometheusSrv ) RouteGetRuleStatuses ( c * models . ReqContext ) response . Response {
2022-04-11 10:54:29 -04:00
dashboardUID := c . Query ( "dashboard_uid" )
panelID , err := getPanelIDFromRequest ( c . Req )
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "invalid panel_id" )
}
if dashboardUID == "" && panelID != 0 {
return ErrResp ( http . StatusBadRequest , errors . New ( "panel_id must be set with dashboard_uid" ), "" )
}
2021-04-13 14:38:09 -07:00
ruleResponse := apimodels . RuleResponse {
DiscoveryBase : apimodels . DiscoveryBase {
Status : "success" ,
},
2021-04-23 11:47:52 -07:00
Data : apimodels . RuleDiscovery {
RuleGroups : [] * apimodels . RuleGroup {},
},
2021-04-13 14:38:09 -07:00
}
2022-03-16 16:04:19 +00:00
var labelOptions [] ngmodels . LabelOption
if ! c . QueryBoolWithDefault ( queryIncludeInternalLabels , false ) {
labelOptions = append ( labelOptions , ngmodels . WithoutInternalLabels ())
}
2022-08-11 13:28:55 +02:00
namespaceMap , err := srv . store . GetUserVisibleNamespaces ( c . Req . Context (), c . OrgID , c . SignedInUser )
2021-07-22 09:53:14 +03:00
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get namespaces visible to the user" )
}
2021-11-08 13:26:08 +00:00
if len ( namespaceMap ) == 0 {
2022-06-07 18:54:23 +01:00
srv . log . Debug ( "user does not have access to any namespaces" )
2021-11-08 13:26:08 +00:00
return response . JSON ( http . StatusOK , ruleResponse )
}
2021-07-22 09:53:14 +03:00
namespaceUIDs := make ([] string , len ( namespaceMap ))
for k := range namespaceMap {
namespaceUIDs = append ( namespaceUIDs , k )
}
2022-04-11 10:54:29 -04:00
alertRuleQuery := ngmodels . ListAlertRulesQuery {
2022-08-11 13:28:55 +02:00
OrgID : c . SignedInUser . OrgID ,
2021-07-22 09:53:14 +03:00
NamespaceUIDs : namespaceUIDs ,
2021-10-04 16:33:55 +01:00
DashboardUID : dashboardUID ,
PanelID : panelID ,
2021-04-13 14:38:09 -07:00
}
2022-04-25 11:42:42 +01:00
if err := srv . store . ListAlertRules ( c . Req . Context (), & alertRuleQuery ); err != nil {
2021-10-01 16:39:04 +03:00
ruleResponse . DiscoveryBase . Status = "error"
ruleResponse . DiscoveryBase . Error = fmt . Sprintf ( "failure getting rules: %s" , err . Error ())
ruleResponse . DiscoveryBase . ErrorType = apiv1 . ErrServer
return response . JSON ( http . StatusInternalServerError , ruleResponse )
}
2022-04-11 17:37:44 -04:00
hasAccess := func ( evaluator accesscontrol . Evaluator ) bool {
2022-05-19 09:22:26 -04:00
return accesscontrol . HasAccess ( srv . ac , c )( accesscontrol . ReqViewer , evaluator )
2022-04-11 17:37:44 -04:00
}
2021-10-01 16:39:04 +03:00
2022-05-16 15:45:45 -04:00
groupedRules := make ( map [ ngmodels . AlertRuleGroupKey ][] * ngmodels . AlertRule )
2021-10-01 16:39:04 +03:00
for _ , rule := range alertRuleQuery . Result {
2022-05-16 15:45:45 -04:00
key := rule . GetGroupKey ()
rulesInGroup := groupedRules [ key ]
rulesInGroup = append ( rulesInGroup , rule )
groupedRules [ key ] = rulesInGroup
}
for groupKey , rules := range groupedRules {
folder := namespaceMap [ groupKey . NamespaceUID ]
if folder == nil {
srv . log . Warn ( "query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response" , "folder_uid" , groupKey . NamespaceUID )
continue
2021-10-01 16:39:04 +03:00
}
2022-06-01 10:23:54 -04:00
if ! authorizeAccessToRuleGroup ( rules , hasAccess ) {
continue
}
2022-05-16 15:45:45 -04:00
ruleResponse . Data . RuleGroups = append ( ruleResponse . Data . RuleGroups , srv . toRuleGroup ( groupKey . RuleGroup , folder , rules , labelOptions ))
}
return response . JSON ( http . StatusOK , ruleResponse )
}
func ( srv PrometheusSrv ) toRuleGroup ( groupName string , folder * models . Folder , rules [] * ngmodels . AlertRule , labelOptions [] ngmodels . LabelOption ) * apimodels . RuleGroup {
newGroup := & apimodels . RuleGroup {
Name : groupName ,
File : folder . Title , // file is what Prometheus uses for provisioning, we replace it with namespace.
}
2022-06-22 10:52:46 -04:00
ngmodels . RulesGroup ( rules ). SortByGroupIndex ()
2022-05-16 15:45:45 -04:00
for _ , rule := range rules {
2021-10-01 16:39:04 +03:00
alertingRule := apimodels . AlertingRule {
State : "inactive" ,
Name : rule . Title ,
2022-03-14 10:39:20 +00:00
Query : ruleToQuery ( srv . log , rule ),
2021-10-01 16:39:04 +03:00
Duration : rule . For . Seconds (),
Annotations : rule . Annotations ,
}
newRule := apimodels . Rule {
Name : rule . Title ,
2022-03-16 16:04:19 +00:00
Labels : rule . GetLabels ( labelOptions ... ),
2021-10-01 16:39:04 +03:00
Health : "ok" ,
Type : apiv1 . RuleTypeAlerting ,
LastEvaluation : time . Time {},
}
2022-05-16 15:45:45 -04:00
for _ , alertState := range srv . manager . GetStatesForRuleUID ( rule . OrgID , rule . UID ) {
2021-10-01 16:39:04 +03:00
activeAt := alertState . StartsAt
valString := ""
2022-04-05 19:36:42 +01:00
if alertState . State == eval . Alerting || alertState . State == eval . Pending {
valString = formatValues ( alertState )
2021-04-27 16:15:00 -04:00
}
2022-03-16 16:04:19 +00:00
2021-10-01 16:39:04 +03:00
alert := & apimodels . Alert {
2022-03-16 16:04:19 +00:00
Labels : alertState . GetLabels ( labelOptions ... ),
2021-10-01 16:39:04 +03:00
Annotations : alertState . Annotations ,
2022-05-23 16:49:49 +08:00
// TODO: or should we make this two fields? Using one field lets the
// frontend use the same logic for parsing text on annotations and this.
State : state . InstanceStateAndReason {
State : alertState . State ,
Reason : alertState . StateReason ,
}. String (),
ActiveAt : & activeAt ,
Value : valString ,
2021-04-13 14:38:09 -07:00
}
2021-10-01 16:39:04 +03:00
if alertState . LastEvaluationTime . After ( newRule . LastEvaluation ) {
newRule . LastEvaluation = alertState . LastEvaluationTime
2021-04-13 14:38:09 -07:00
}
2021-04-21 09:30:03 -07:00
2021-10-01 16:39:04 +03:00
newRule . EvaluationTime = alertState . EvaluationDuration . Seconds ()
2021-05-04 10:08:12 -07:00
2021-10-01 16:39:04 +03:00
switch alertState . State {
case eval . Normal :
case eval . Pending :
if alertingRule . State == "inactive" {
alertingRule . State = "pending"
2021-05-04 10:08:12 -07:00
}
2021-10-01 16:39:04 +03:00
case eval . Alerting :
alertingRule . State = "firing"
case eval . Error :
newRule . Health = "error"
case eval . NoData :
newRule . Health = "nodata"
2021-04-13 14:38:09 -07:00
}
2021-04-21 09:30:03 -07:00
2021-10-01 16:39:04 +03:00
if alertState . Error != nil {
newRule . LastError = alertState . Error . Error ()
newRule . Health = "error"
}
2022-03-14 10:39:20 +00:00
2021-10-01 16:39:04 +03:00
alertingRule . Alerts = append ( alertingRule . Alerts , alert )
2021-04-13 14:38:09 -07:00
}
2021-10-01 16:39:04 +03:00
alertingRule . Rule = newRule
newGroup . Rules = append ( newGroup . Rules , alertingRule )
newGroup . Interval = float64 ( rule . IntervalSeconds )
2022-05-16 15:45:45 -04:00
// TODO yuri. Change that when scheduler will process alerts in groups
2022-04-11 10:54:29 -04:00
newGroup . EvaluationTime = newRule . EvaluationTime
newGroup . LastEvaluation = newRule . LastEvaluation
2021-04-13 14:38:09 -07:00
}
2022-05-16 15:45:45 -04:00
return newGroup
2021-04-13 14:38:09 -07:00
}
2022-03-14 10:39:20 +00:00
// ruleToQuery attempts to extract the datasource queries from the alert query model.
// Returns the whole JSON model as a string if it fails to extract a minimum of 1 query.
func ruleToQuery ( logger log . Logger , rule * ngmodels . AlertRule ) string {
var queryErr error
var queries [] string
for _ , q := range rule . Data {
q , err := q . GetQuery ()
if err != nil {
// If we can't find the query simply omit it, and try the rest.
// Even single query alerts would have 2 `AlertQuery`, one for the query and one for the condition.
if errors . Is ( err , ngmodels . ErrNoQuery ) {
continue
}
// For any other type of error, it is unexpected abort and return the whole JSON.
logger . Debug ( "failed to parse a query" , "err" , err )
queryErr = err
break
}
queries = append ( queries , q )
}
// If we were able to extract at least one query without failure use it.
if queryErr == nil && len ( queries ) > 0 {
return strings . Join ( queries , " | " )
}
return encodedQueriesOrError ( rule . Data )
}
// encodedQueriesOrError tries to encode rule query data into JSON if it fails returns the encoding error as a string.
func encodedQueriesOrError ( rules [] ngmodels . AlertQuery ) string {
encodedQueries , err := json . Marshal ( rules )
if err == nil {
return string ( encodedQueries )
}
return err . Error ()
}