2021-04-05 17:05:39 -05:00
package api
import (
2021-04-27 15:15:00 -05:00
"encoding/json"
2021-10-04 10:33:55 -05:00
"errors"
2021-04-13 16:38:09 -05:00
"fmt"
2021-04-05 17:05:39 -05:00
"net/http"
2022-04-05 13:36:42 -05:00
"sort"
2021-10-04 10:33:55 -05:00
"strconv"
"strings"
2021-04-13 16:38:09 -05:00
"time"
2021-04-05 17:05:39 -05:00
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
2022-04-11 16:37:44 -05:00
"github.com/grafana/grafana/pkg/services/accesscontrol"
2021-04-19 13:26:04 -05:00
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
2022-03-16 11:04:19 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/eval"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2021-04-05 17:05:39 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/state"
2022-03-16 11:04:19 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/store"
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
2021-04-05 17:05:39 -05:00
)
type PrometheusSrv struct {
2021-04-23 14:32:25 -05:00
log log . Logger
2022-03-09 12:20:29 -06:00
manager state . AlertInstanceManager
2021-04-23 14:32:25 -05:00
store store . RuleStore
2022-04-11 16:37:44 -05:00
ac accesscontrol . AccessControl
2021-04-05 17:05:39 -05:00
}
2022-03-16 11:04:19 -05:00
const queryIncludeInternalLabels = "includeInternalLabels"
2021-04-05 17:05:39 -05: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 11:04:19 -05:00
var labelOptions [ ] ngmodels . LabelOption
if ! c . QueryBoolWithDefault ( queryIncludeInternalLabels , false ) {
labelOptions = append ( labelOptions , ngmodels . WithoutInternalLabels ( ) )
}
2022-08-11 06:28:55 -05:00
for _ , alertState := range srv . manager . GetAll ( c . OrgID ) {
2021-04-05 17:05:39 -05:00
startsAt := alertState . StartsAt
2021-05-18 08:12:39 -05:00
valString := ""
2022-04-05 13:36:42 -05:00
if alertState . State == eval . Alerting || alertState . State == eval . Pending {
valString = formatValues ( alertState )
2021-05-18 08:12:39 -05:00
}
2022-03-09 12:20:29 -06:00
2021-04-05 17:05:39 -05:00
alertResponse . Data . Alerts = append ( alertResponse . Data . Alerts , & apimodels . Alert {
2022-03-16 11:04:19 -05:00
Labels : alertState . GetLabels ( labelOptions ... ) ,
2022-03-09 12:20:29 -06:00
Annotations : alertState . Annotations ,
2022-05-23 03:49:49 -05: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 17:05:39 -05:00
} )
}
2022-03-09 12:20:29 -06:00
2021-04-05 17:05:39 -05:00
return response . JSON ( http . StatusOK , alertResponse )
}
2022-04-05 13:36:42 -05: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 10:33:55 -05: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 17:05:39 -05:00
func ( srv PrometheusSrv ) RouteGetRuleStatuses ( c * models . ReqContext ) response . Response {
2022-04-11 09:54:29 -05: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 16:38:09 -05:00
ruleResponse := apimodels . RuleResponse {
DiscoveryBase : apimodels . DiscoveryBase {
Status : "success" ,
} ,
2021-04-23 13:47:52 -05:00
Data : apimodels . RuleDiscovery {
RuleGroups : [ ] * apimodels . RuleGroup { } ,
} ,
2021-04-13 16:38:09 -05:00
}
2022-03-16 11:04:19 -05:00
var labelOptions [ ] ngmodels . LabelOption
if ! c . QueryBoolWithDefault ( queryIncludeInternalLabels , false ) {
labelOptions = append ( labelOptions , ngmodels . WithoutInternalLabels ( ) )
}
2022-08-11 06:28:55 -05:00
namespaceMap , err := srv . store . GetUserVisibleNamespaces ( c . Req . Context ( ) , c . OrgID , c . SignedInUser )
2021-07-22 01:53:14 -05:00
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get namespaces visible to the user" )
}
2021-11-08 07:26:08 -06:00
if len ( namespaceMap ) == 0 {
2022-06-07 12:54:23 -05:00
srv . log . Debug ( "user does not have access to any namespaces" )
2021-11-08 07:26:08 -06:00
return response . JSON ( http . StatusOK , ruleResponse )
}
2021-07-22 01:53:14 -05:00
namespaceUIDs := make ( [ ] string , len ( namespaceMap ) )
for k := range namespaceMap {
namespaceUIDs = append ( namespaceUIDs , k )
}
2022-04-11 09:54:29 -05:00
alertRuleQuery := ngmodels . ListAlertRulesQuery {
2022-08-11 06:28:55 -05:00
OrgID : c . SignedInUser . OrgID ,
2021-07-22 01:53:14 -05:00
NamespaceUIDs : namespaceUIDs ,
2021-10-04 10:33:55 -05:00
DashboardUID : dashboardUID ,
PanelID : panelID ,
2021-04-13 16:38:09 -05:00
}
2022-04-25 05:42:42 -05:00
if err := srv . store . ListAlertRules ( c . Req . Context ( ) , & alertRuleQuery ) ; err != nil {
2021-10-01 08:39:04 -05: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 16:37:44 -05:00
hasAccess := func ( evaluator accesscontrol . Evaluator ) bool {
2022-05-19 08:22:26 -05:00
return accesscontrol . HasAccess ( srv . ac , c ) ( accesscontrol . ReqViewer , evaluator )
2022-04-11 16:37:44 -05:00
}
2021-10-01 08:39:04 -05:00
2022-05-16 14:45:45 -05:00
groupedRules := make ( map [ ngmodels . AlertRuleGroupKey ] [ ] * ngmodels . AlertRule )
2021-10-01 08:39:04 -05:00
for _ , rule := range alertRuleQuery . Result {
2022-05-16 14:45:45 -05: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 08:39:04 -05:00
}
2022-06-01 09:23:54 -05:00
if ! authorizeAccessToRuleGroup ( rules , hasAccess ) {
continue
}
2022-05-16 14:45:45 -05: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 09:52:46 -05:00
ngmodels . RulesGroup ( rules ) . SortByGroupIndex ( )
2022-05-16 14:45:45 -05:00
for _ , rule := range rules {
2021-10-01 08:39:04 -05:00
alertingRule := apimodels . AlertingRule {
State : "inactive" ,
Name : rule . Title ,
2022-03-14 05:39:20 -05:00
Query : ruleToQuery ( srv . log , rule ) ,
2021-10-01 08:39:04 -05:00
Duration : rule . For . Seconds ( ) ,
Annotations : rule . Annotations ,
}
newRule := apimodels . Rule {
Name : rule . Title ,
2022-03-16 11:04:19 -05:00
Labels : rule . GetLabels ( labelOptions ... ) ,
2021-10-01 08:39:04 -05:00
Health : "ok" ,
Type : apiv1 . RuleTypeAlerting ,
LastEvaluation : time . Time { } ,
}
2022-05-16 14:45:45 -05:00
for _ , alertState := range srv . manager . GetStatesForRuleUID ( rule . OrgID , rule . UID ) {
2021-10-01 08:39:04 -05:00
activeAt := alertState . StartsAt
valString := ""
2022-04-05 13:36:42 -05:00
if alertState . State == eval . Alerting || alertState . State == eval . Pending {
valString = formatValues ( alertState )
2021-04-27 15:15:00 -05:00
}
2022-03-16 11:04:19 -05:00
2021-10-01 08:39:04 -05:00
alert := & apimodels . Alert {
2022-03-16 11:04:19 -05:00
Labels : alertState . GetLabels ( labelOptions ... ) ,
2021-10-01 08:39:04 -05:00
Annotations : alertState . Annotations ,
2022-05-23 03:49:49 -05: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 16:38:09 -05:00
}
2021-10-01 08:39:04 -05:00
if alertState . LastEvaluationTime . After ( newRule . LastEvaluation ) {
newRule . LastEvaluation = alertState . LastEvaluationTime
2021-04-13 16:38:09 -05:00
}
2021-04-21 11:30:03 -05:00
2021-10-01 08:39:04 -05:00
newRule . EvaluationTime = alertState . EvaluationDuration . Seconds ( )
2021-05-04 12:08:12 -05:00
2021-10-01 08:39:04 -05:00
switch alertState . State {
case eval . Normal :
case eval . Pending :
if alertingRule . State == "inactive" {
alertingRule . State = "pending"
2021-05-04 12:08:12 -05:00
}
2021-10-01 08:39:04 -05:00
case eval . Alerting :
alertingRule . State = "firing"
case eval . Error :
newRule . Health = "error"
case eval . NoData :
newRule . Health = "nodata"
2021-04-13 16:38:09 -05:00
}
2021-04-21 11:30:03 -05:00
2021-10-01 08:39:04 -05:00
if alertState . Error != nil {
newRule . LastError = alertState . Error . Error ( )
newRule . Health = "error"
}
2022-03-14 05:39:20 -05:00
2021-10-01 08:39:04 -05:00
alertingRule . Alerts = append ( alertingRule . Alerts , alert )
2021-04-13 16:38:09 -05:00
}
2021-10-01 08:39:04 -05:00
alertingRule . Rule = newRule
newGroup . Rules = append ( newGroup . Rules , alertingRule )
newGroup . Interval = float64 ( rule . IntervalSeconds )
2022-05-16 14:45:45 -05:00
// TODO yuri. Change that when scheduler will process alerts in groups
2022-04-11 09:54:29 -05:00
newGroup . EvaluationTime = newRule . EvaluationTime
newGroup . LastEvaluation = newRule . LastEvaluation
2021-04-13 16:38:09 -05:00
}
2022-05-16 14:45:45 -05:00
return newGroup
2021-04-13 16:38:09 -05:00
}
2022-03-14 05:39:20 -05: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 ( )
}