2022-06-02 07:48:53 -05:00
package provisioning
import (
"context"
"errors"
"fmt"
2023-01-27 10:39:16 -06:00
"sort"
2022-06-02 07:48:53 -05:00
"time"
"github.com/grafana/grafana/pkg/infra/log"
2023-01-27 10:39:16 -06:00
"github.com/grafana/grafana/pkg/services/dashboards"
2022-06-02 07:48:53 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
2023-01-27 10:39:16 -06:00
"github.com/grafana/grafana/pkg/services/provisioning/alerting/file"
2022-07-13 17:36:17 -05:00
"github.com/grafana/grafana/pkg/services/quota"
2022-06-02 07:48:53 -05:00
"github.com/grafana/grafana/pkg/util"
)
type AlertRuleService struct {
2022-06-09 02:28:32 -05:00
defaultIntervalSeconds int64
baseIntervalSeconds int64
2022-06-09 03:38:46 -05:00
ruleStore RuleStore
2022-06-09 02:28:32 -05:00
provenanceStore ProvisioningStore
2023-01-27 10:39:16 -06:00
dashboardService dashboards . DashboardService
2022-07-13 17:36:17 -05:00
quotas QuotaChecker
2022-06-09 02:28:32 -05:00
xact TransactionManager
log log . Logger
2022-06-02 07:48:53 -05:00
}
2022-06-09 03:38:46 -05:00
func NewAlertRuleService ( ruleStore RuleStore ,
2022-06-02 07:48:53 -05:00
provenanceStore ProvisioningStore ,
2023-01-27 10:39:16 -06:00
dashboardService dashboards . DashboardService ,
2022-07-13 17:36:17 -05:00
quotas QuotaChecker ,
2022-06-02 07:48:53 -05:00
xact TransactionManager ,
2022-06-09 02:28:32 -05:00
defaultIntervalSeconds int64 ,
baseIntervalSeconds int64 ,
2022-06-02 07:48:53 -05:00
log log . Logger ) * AlertRuleService {
return & AlertRuleService {
2022-06-09 02:28:32 -05:00
defaultIntervalSeconds : defaultIntervalSeconds ,
baseIntervalSeconds : baseIntervalSeconds ,
ruleStore : ruleStore ,
provenanceStore : provenanceStore ,
2023-01-27 10:39:16 -06:00
dashboardService : dashboardService ,
2022-07-13 17:36:17 -05:00
quotas : quotas ,
2022-06-09 02:28:32 -05:00
xact : xact ,
log : log ,
2022-06-02 07:48:53 -05:00
}
}
2022-12-13 04:54:08 -06:00
func ( service * AlertRuleService ) GetAlertRules ( ctx context . Context , orgID int64 ) ( [ ] * models . AlertRule , error ) {
q := models . ListAlertRulesQuery {
OrgID : orgID ,
}
err := service . ruleStore . ListAlertRules ( ctx , & q )
if err != nil {
return nil , err
}
// TODO: GET provenance
return q . Result , nil
}
2022-06-02 07:48:53 -05:00
func ( service * AlertRuleService ) GetAlertRule ( ctx context . Context , orgID int64 , ruleUID string ) ( models . AlertRule , models . Provenance , error ) {
query := & models . GetAlertRuleByUIDQuery {
OrgID : orgID ,
UID : ruleUID ,
}
err := service . ruleStore . GetAlertRuleByUID ( ctx , query )
if err != nil {
return models . AlertRule { } , models . ProvenanceNone , err
}
provenance , err := service . provenanceStore . GetProvenance ( ctx , query . Result , orgID )
if err != nil {
return models . AlertRule { } , models . ProvenanceNone , err
}
return * query . Result , provenance , nil
}
2023-01-27 10:39:16 -06:00
type AlertRuleWithFolderTitle struct {
AlertRule models . AlertRule
FolderTitle string
}
// GetAlertRuleWithFolderTitle returns a single alert rule with its folder title.
func ( service * AlertRuleService ) GetAlertRuleWithFolderTitle ( ctx context . Context , orgID int64 , ruleUID string ) ( AlertRuleWithFolderTitle , error ) {
query := & models . GetAlertRuleByUIDQuery {
OrgID : orgID ,
UID : ruleUID ,
}
err := service . ruleStore . GetAlertRuleByUID ( ctx , query )
if err != nil {
return AlertRuleWithFolderTitle { } , err
}
dq := dashboards . GetDashboardQuery {
OrgID : orgID ,
UID : query . Result . NamespaceUID ,
}
dash , err := service . dashboardService . GetDashboard ( ctx , & dq )
if err != nil {
return AlertRuleWithFolderTitle { } , err
}
return AlertRuleWithFolderTitle {
AlertRule : * query . Result ,
FolderTitle : dash . Title ,
} , nil
}
2022-06-10 09:25:15 -05:00
// CreateAlertRule creates a new alert rule. This function will ignore any
// interval that is set in the rule struct and use the already existing group
// interval or the default one.
2022-07-13 17:36:17 -05:00
func ( service * AlertRuleService ) CreateAlertRule ( ctx context . Context , rule models . AlertRule , provenance models . Provenance , userID int64 ) ( models . AlertRule , error ) {
2022-06-02 07:48:53 -05:00
if rule . UID == "" {
rule . UID = util . GenerateShortUID ( )
}
interval , err := service . ruleStore . GetRuleGroupInterval ( ctx , rule . OrgID , rule . NamespaceUID , rule . RuleGroup )
// if the alert group does not exists we just use the default interval
if err != nil && errors . Is ( err , store . ErrAlertRuleGroupNotFound ) {
2022-06-09 02:28:32 -05:00
interval = service . defaultIntervalSeconds
2022-06-02 07:48:53 -05:00
} else if err != nil {
return models . AlertRule { } , err
}
rule . IntervalSeconds = interval
2022-12-16 04:47:25 -06:00
err = rule . SetDashboardAndPanelFromAnnotations ( )
2022-08-03 09:05:32 -05:00
if err != nil {
return models . AlertRule { } , err
}
2022-06-02 07:48:53 -05:00
rule . Updated = time . Now ( )
err = service . xact . InTransaction ( ctx , func ( ctx context . Context ) error {
ids , err := service . ruleStore . InsertAlertRules ( ctx , [ ] models . AlertRule {
rule ,
} )
if err != nil {
return err
}
if id , ok := ids [ rule . UID ] ; ok {
rule . ID = id
} else {
return errors . New ( "couldn't find newly created id" )
}
2022-07-13 17:36:17 -05:00
2022-08-10 12:33:41 -05:00
if err = service . checkLimitsTransactionCtx ( ctx , rule . OrgID , userID ) ; err != nil {
return err
2022-07-13 17:36:17 -05:00
}
2022-06-02 07:48:53 -05:00
return service . provenanceStore . SetProvenance ( ctx , & rule , rule . OrgID , provenance )
} )
if err != nil {
return models . AlertRule { } , err
}
return rule , nil
}
2023-01-27 10:39:16 -06:00
func ( service * AlertRuleService ) GetRuleGroup ( ctx context . Context , orgID int64 , namespaceUID , group string ) ( models . AlertRuleGroup , error ) {
2022-07-05 11:53:50 -05:00
q := models . ListAlertRulesQuery {
OrgID : orgID ,
2023-01-27 10:39:16 -06:00
NamespaceUIDs : [ ] string { namespaceUID } ,
2022-07-05 11:53:50 -05:00
RuleGroup : group ,
}
if err := service . ruleStore . ListAlertRules ( ctx , & q ) ; err != nil {
2022-08-12 16:36:50 -05:00
return models . AlertRuleGroup { } , err
2022-07-05 11:53:50 -05:00
}
if len ( q . Result ) == 0 {
2022-08-12 16:36:50 -05:00
return models . AlertRuleGroup { } , store . ErrAlertRuleGroupNotFound
2022-07-05 11:53:50 -05:00
}
2022-08-12 16:36:50 -05:00
res := models . AlertRuleGroup {
2022-07-05 11:53:50 -05:00
Title : q . Result [ 0 ] . RuleGroup ,
FolderUID : q . Result [ 0 ] . NamespaceUID ,
Interval : q . Result [ 0 ] . IntervalSeconds ,
Rules : [ ] models . AlertRule { } ,
}
for _ , r := range q . Result {
if r != nil {
res . Rules = append ( res . Rules , * r )
}
}
return res , nil
}
2022-06-09 02:28:32 -05:00
// UpdateRuleGroup will update the interval for all rules in the group.
2022-07-14 16:53:13 -05:00
func ( service * AlertRuleService ) UpdateRuleGroup ( ctx context . Context , orgID int64 , namespaceUID string , ruleGroup string , intervalSeconds int64 ) error {
if err := models . ValidateRuleGroupInterval ( intervalSeconds , service . baseIntervalSeconds ) ; err != nil {
2022-06-09 02:28:32 -05:00
return err
}
return service . xact . InTransaction ( ctx , func ( ctx context . Context ) error {
query := & models . ListAlertRulesQuery {
OrgID : orgID ,
NamespaceUIDs : [ ] string { namespaceUID } ,
RuleGroup : ruleGroup ,
}
err := service . ruleStore . ListAlertRules ( ctx , query )
if err != nil {
return fmt . Errorf ( "failed to list alert rules: %w" , err )
}
2022-09-29 15:47:56 -05:00
updateRules := make ( [ ] models . UpdateRule , 0 , len ( query . Result ) )
2022-06-09 02:28:32 -05:00
for _ , rule := range query . Result {
2022-07-14 16:53:13 -05:00
if rule . IntervalSeconds == intervalSeconds {
2022-06-09 02:28:32 -05:00
continue
}
newRule := * rule
2022-07-14 16:53:13 -05:00
newRule . IntervalSeconds = intervalSeconds
2022-09-29 15:47:56 -05:00
updateRules = append ( updateRules , models . UpdateRule {
2022-06-09 02:28:32 -05:00
Existing : rule ,
New : newRule ,
} )
}
return service . ruleStore . UpdateAlertRules ( ctx , updateRules )
} )
}
2022-08-12 16:36:50 -05:00
func ( service * AlertRuleService ) ReplaceRuleGroup ( ctx context . Context , orgID int64 , group models . AlertRuleGroup , userID int64 , provenance models . Provenance ) error {
2022-08-10 12:33:41 -05:00
if err := models . ValidateRuleGroupInterval ( group . Interval , service . baseIntervalSeconds ) ; err != nil {
return err
}
// If the provided request did not provide the rules list at all, treat it as though it does not wish to change rules.
// This is done for backwards compatibility. Requests which specify only the interval must update only the interval.
if group . Rules == nil {
listRulesQuery := models . ListAlertRulesQuery {
OrgID : orgID ,
NamespaceUIDs : [ ] string { group . FolderUID } ,
RuleGroup : group . Title ,
}
if err := service . ruleStore . ListAlertRules ( ctx , & listRulesQuery ) ; err != nil {
return fmt . Errorf ( "failed to list alert rules: %w" , err )
}
group . Rules = make ( [ ] models . AlertRule , 0 , len ( listRulesQuery . Result ) )
for _ , r := range listRulesQuery . Result {
if r != nil {
group . Rules = append ( group . Rules , * r )
}
}
}
key := models . AlertRuleGroupKey {
OrgID : orgID ,
NamespaceUID : group . FolderUID ,
RuleGroup : group . Title ,
}
2023-02-01 06:15:03 -06:00
rules := make ( [ ] * models . AlertRuleWithOptionals , len ( group . Rules ) )
2022-08-10 12:33:41 -05:00
group = * syncGroupRuleFields ( & group , orgID )
for i := range group . Rules {
2022-12-16 04:47:25 -06:00
if err := group . Rules [ i ] . SetDashboardAndPanelFromAnnotations ( ) ; err != nil {
return err
}
2023-02-01 06:15:03 -06:00
rules = append ( rules , & models . AlertRuleWithOptionals { AlertRule : group . Rules [ i ] , HasPause : true } )
2022-08-10 12:33:41 -05:00
}
delta , err := store . CalculateChanges ( ctx , service . ruleStore , key , rules )
if err != nil {
return fmt . Errorf ( "failed to calculate diff for alert rules: %w" , err )
}
// Refresh all calculated fields across all rules.
delta = store . UpdateCalculatedRuleFields ( delta )
if len ( delta . New ) == 0 && len ( delta . Update ) == 0 && len ( delta . Delete ) == 0 {
return nil
}
return service . xact . InTransaction ( ctx , func ( ctx context . Context ) error {
uids , err := service . ruleStore . InsertAlertRules ( ctx , withoutNilAlertRules ( delta . New ) )
if err != nil {
return fmt . Errorf ( "failed to insert alert rules: %w" , err )
}
for uid := range uids {
if err := service . provenanceStore . SetProvenance ( ctx , & models . AlertRule { UID : uid } , orgID , provenance ) ; err != nil {
return err
}
}
2022-09-29 15:47:56 -05:00
updates := make ( [ ] models . UpdateRule , 0 , len ( delta . Update ) )
2022-08-10 12:33:41 -05:00
for _ , update := range delta . Update {
// check that provenance is not changed in a invalid way
storedProvenance , err := service . provenanceStore . GetProvenance ( ctx , update . New , orgID )
if err != nil {
return err
}
if storedProvenance != provenance && storedProvenance != models . ProvenanceNone {
return fmt . Errorf ( "cannot update with provided provenance '%s', needs '%s'" , provenance , storedProvenance )
}
2022-09-29 15:47:56 -05:00
updates = append ( updates , models . UpdateRule {
2022-08-10 12:33:41 -05:00
Existing : update . Existing ,
New : * update . New ,
} )
}
if err = service . ruleStore . UpdateAlertRules ( ctx , updates ) ; err != nil {
return fmt . Errorf ( "failed to update alert rules: %w" , err )
}
for _ , update := range delta . Update {
if err := service . provenanceStore . SetProvenance ( ctx , update . New , orgID , provenance ) ; err != nil {
return err
}
}
for _ , delete := range delta . Delete {
// check that provenance is not changed in a invalid way
storedProvenance , err := service . provenanceStore . GetProvenance ( ctx , delete , orgID )
if err != nil {
return err
}
if storedProvenance != provenance && storedProvenance != models . ProvenanceNone {
return fmt . Errorf ( "cannot update with provided provenance '%s', needs '%s'" , provenance , storedProvenance )
}
}
if err := service . deleteRules ( ctx , orgID , delta . Delete ... ) ; err != nil {
return err
}
if err = service . checkLimitsTransactionCtx ( ctx , orgID , userID ) ; err != nil {
return err
}
return nil
} )
}
2022-06-10 09:25:15 -05:00
// CreateAlertRule creates a new alert rule. This function will ignore any
// interval that is set in the rule struct and fetch the current group interval
// from database.
2022-06-02 07:48:53 -05:00
func ( service * AlertRuleService ) UpdateAlertRule ( ctx context . Context , rule models . AlertRule , provenance models . Provenance ) ( models . AlertRule , error ) {
storedRule , storedProvenance , err := service . GetAlertRule ( ctx , rule . OrgID , rule . UID )
if err != nil {
return models . AlertRule { } , err
}
if storedProvenance != provenance && storedProvenance != models . ProvenanceNone {
return models . AlertRule { } , fmt . Errorf ( "cannot changed provenance from '%s' to '%s'" , storedProvenance , provenance )
}
rule . Updated = time . Now ( )
rule . ID = storedRule . ID
2022-08-11 17:54:57 -05:00
rule . IntervalSeconds = storedRule . IntervalSeconds
2022-12-16 04:47:25 -06:00
err = rule . SetDashboardAndPanelFromAnnotations ( )
2022-08-03 09:05:32 -05:00
if err != nil {
return models . AlertRule { } , err
}
2022-06-02 07:48:53 -05:00
err = service . xact . InTransaction ( ctx , func ( ctx context . Context ) error {
2022-09-29 15:47:56 -05:00
err := service . ruleStore . UpdateAlertRules ( ctx , [ ] models . UpdateRule {
2022-06-02 07:48:53 -05:00
{
Existing : & storedRule ,
New : rule ,
} ,
} )
if err != nil {
return err
}
return service . provenanceStore . SetProvenance ( ctx , & rule , rule . OrgID , provenance )
} )
if err != nil {
return models . AlertRule { } , err
}
return rule , err
}
func ( service * AlertRuleService ) DeleteAlertRule ( ctx context . Context , orgID int64 , ruleUID string , provenance models . Provenance ) error {
rule := & models . AlertRule {
OrgID : orgID ,
UID : ruleUID ,
}
// check that provenance is not changed in a invalid way
storedProvenance , err := service . provenanceStore . GetProvenance ( ctx , rule , rule . OrgID )
if err != nil {
return err
}
if storedProvenance != provenance && storedProvenance != models . ProvenanceNone {
return fmt . Errorf ( "cannot delete with provided provenance '%s', needs '%s'" , provenance , storedProvenance )
}
return service . xact . InTransaction ( ctx , func ( ctx context . Context ) error {
2022-08-10 12:33:41 -05:00
return service . deleteRules ( ctx , orgID , rule )
} )
}
// checkLimitsTransactionCtx checks whether the current transaction (as identified by the ctx) breaches configured alert rule limits.
func ( service * AlertRuleService ) checkLimitsTransactionCtx ( ctx context . Context , orgID , userID int64 ) error {
2022-12-08 08:34:46 -06:00
limitReached , err := service . quotas . CheckQuotaReached ( ctx , models . QuotaTargetSrv , & quota . ScopeParameters {
2022-08-10 12:33:41 -05:00
OrgID : orgID ,
UserID : userID ,
2022-06-02 07:48:53 -05:00
} )
2022-08-10 12:33:41 -05:00
if err != nil {
return fmt . Errorf ( "failed to check alert rule quota: %w" , err )
}
if limitReached {
return models . ErrQuotaReached
}
return nil
}
// deleteRules deletes a set of target rules and associated data, while checking for database consistency.
func ( service * AlertRuleService ) deleteRules ( ctx context . Context , orgID int64 , targets ... * models . AlertRule ) error {
uids := make ( [ ] string , 0 , len ( targets ) )
for _ , tgt := range targets {
if tgt != nil {
uids = append ( uids , tgt . UID )
}
}
if err := service . ruleStore . DeleteAlertRulesByUID ( ctx , orgID , uids ... ) ; err != nil {
return err
}
for _ , uid := range uids {
if err := service . provenanceStore . DeleteProvenance ( ctx , & models . AlertRule { UID : uid } , orgID ) ; err != nil {
// We failed to clean up the record, but this doesn't break things. Log it and move on.
service . log . Warn ( "failed to delete provenance record for rule: %w" , err )
}
}
return nil
}
2023-01-27 10:39:16 -06:00
// GetAlertRuleGroupWithFolderTitle returns the alert rule group with folder title.
func ( service * AlertRuleService ) GetAlertRuleGroupWithFolderTitle ( ctx context . Context , orgID int64 , namespaceUID , group string ) ( file . AlertRuleGroupWithFolderTitle , error ) {
q := models . ListAlertRulesQuery {
OrgID : orgID ,
NamespaceUIDs : [ ] string { namespaceUID } ,
RuleGroup : group ,
}
if err := service . ruleStore . ListAlertRules ( ctx , & q ) ; err != nil {
return file . AlertRuleGroupWithFolderTitle { } , err
}
if len ( q . Result ) == 0 {
return file . AlertRuleGroupWithFolderTitle { } , store . ErrAlertRuleGroupNotFound
}
dq := dashboards . GetDashboardQuery {
OrgID : orgID ,
UID : namespaceUID ,
}
dash , err := service . dashboardService . GetDashboard ( ctx , & dq )
if err != nil {
return file . AlertRuleGroupWithFolderTitle { } , err
}
res := file . AlertRuleGroupWithFolderTitle {
AlertRuleGroup : & models . AlertRuleGroup {
Title : q . Result [ 0 ] . RuleGroup ,
FolderUID : q . Result [ 0 ] . NamespaceUID ,
Interval : q . Result [ 0 ] . IntervalSeconds ,
Rules : [ ] models . AlertRule { } ,
} ,
OrgID : orgID ,
FolderTitle : dash . Title ,
}
for _ , r := range q . Result {
if r != nil {
res . AlertRuleGroup . Rules = append ( res . AlertRuleGroup . Rules , * r )
}
}
return res , nil
}
// GetAlertGroupsWithFolderTitle returns all groups with folder title that have at least one alert.
func ( service * AlertRuleService ) GetAlertGroupsWithFolderTitle ( ctx context . Context , orgID int64 ) ( [ ] file . AlertRuleGroupWithFolderTitle , error ) {
q := models . ListAlertRulesQuery {
OrgID : orgID ,
}
if err := service . ruleStore . ListAlertRules ( ctx , & q ) ; err != nil {
return nil , err
}
groups := make ( map [ models . AlertRuleGroupKey ] [ ] models . AlertRule )
namespaces := make ( map [ string ] [ ] * models . AlertRuleGroupKey )
for _ , r := range q . Result {
groupKey := r . GetGroupKey ( )
group := groups [ groupKey ]
group = append ( group , * r )
groups [ groupKey ] = group
namespaces [ r . NamespaceUID ] = append ( namespaces [ r . NamespaceUID ] , & groupKey )
}
dq := dashboards . GetDashboardsQuery {
DashboardUIDs : nil ,
}
for uid := range namespaces {
dq . DashboardUIDs = append ( dq . DashboardUIDs , uid )
}
// We need folder titles for the provisioning file format. We do it this way instead of using GetUserVisibleNamespaces to avoid folder:read permissions that should not apply to those with alert.provisioning:read.
dashes , err := service . dashboardService . GetDashboards ( ctx , & dq )
if err != nil {
return nil , err
}
folderUidToTitle := make ( map [ string ] string )
for _ , dash := range dashes {
folderUidToTitle [ dash . UID ] = dash . Title
}
result := make ( [ ] file . AlertRuleGroupWithFolderTitle , 0 )
for groupKey , rules := range groups {
title , ok := folderUidToTitle [ groupKey . NamespaceUID ]
if ! ok {
return nil , fmt . Errorf ( "cannot find title for folder with uid '%s'" , groupKey . NamespaceUID )
}
result = append ( result , file . AlertRuleGroupWithFolderTitle {
AlertRuleGroup : & models . AlertRuleGroup {
Title : rules [ 0 ] . RuleGroup ,
FolderUID : rules [ 0 ] . NamespaceUID ,
Interval : rules [ 0 ] . IntervalSeconds ,
Rules : rules ,
} ,
OrgID : orgID ,
FolderTitle : title ,
} )
}
// Return results in a stable manner.
sort . SliceStable ( result , func ( i , j int ) bool {
if result [ i ] . AlertRuleGroup . FolderUID == result [ j ] . AlertRuleGroup . FolderUID {
return result [ i ] . AlertRuleGroup . Title < result [ j ] . AlertRuleGroup . Title
}
return result [ i ] . AlertRuleGroup . FolderUID < result [ j ] . AlertRuleGroup . FolderUID
} )
return result , nil
}
2022-08-10 12:33:41 -05:00
// syncRuleGroupFields synchronizes calculated fields across multiple rules in a group.
2022-08-12 16:36:50 -05:00
func syncGroupRuleFields ( group * models . AlertRuleGroup , orgID int64 ) * models . AlertRuleGroup {
2022-08-10 12:33:41 -05:00
for i := range group . Rules {
group . Rules [ i ] . IntervalSeconds = group . Interval
group . Rules [ i ] . RuleGroup = group . Title
group . Rules [ i ] . NamespaceUID = group . FolderUID
group . Rules [ i ] . OrgID = orgID
}
return group
}
func withoutNilAlertRules ( ptrs [ ] * models . AlertRule ) [ ] models . AlertRule {
result := make ( [ ] models . AlertRule , 0 , len ( ptrs ) )
for _ , ptr := range ptrs {
if ptr != nil {
result = append ( result , * ptr )
}
}
return result
2022-06-02 07:48:53 -05:00
}