2021-04-29 12:24:37 -05:00
package ualert
import (
"encoding/json"
"fmt"
"time"
2021-12-01 05:45:27 -06:00
"github.com/grafana/grafana/pkg/expr"
2021-10-07 16:30:06 -05:00
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2021-12-01 05:45:27 -06:00
"github.com/grafana/grafana/pkg/tsdb/graphite"
2021-04-29 12:24:37 -05:00
"github.com/grafana/grafana/pkg/util"
)
type alertRule struct {
2021-08-12 08:04:09 -05:00
OrgID int64 ` xorm:"org_id" `
2021-04-29 12:24:37 -05:00
Title string
Condition string
Data [ ] alertQuery
IntervalSeconds int64
2021-04-30 14:08:01 -05:00
Version int64
2021-08-12 08:04:09 -05:00
UID string ` xorm:"uid" `
NamespaceUID string ` xorm:"namespace_uid" `
2021-04-29 12:24:37 -05:00
RuleGroup string
NoDataState string
ExecErrState string
For duration
Updated time . Time
Annotations map [ string ] string
2021-04-30 14:08:01 -05:00
Labels map [ string ] string // (Labels are not Created in the migration)
}
type alertRuleVersion struct {
RuleOrgID int64 ` xorm:"rule_org_id" `
RuleUID string ` xorm:"rule_uid" `
RuleNamespaceUID string ` xorm:"rule_namespace_uid" `
RuleGroup string
ParentVersion int64
RestoredFrom int64
Version int64
Created time . Time
Title string
Condition string
Data [ ] alertQuery
IntervalSeconds int64
NoDataState string
ExecErrState string
// ideally this field should have been apimodels.ApiDuration
// but this is currently not possible because of circular dependencies
For duration
Annotations map [ string ] string
Labels map [ string ] string
}
func ( a * alertRule ) makeVersion ( ) * alertRuleVersion {
return & alertRuleVersion {
2021-08-12 08:04:09 -05:00
RuleOrgID : a . OrgID ,
RuleUID : a . UID ,
RuleNamespaceUID : a . NamespaceUID ,
2021-04-30 14:08:01 -05:00
RuleGroup : a . RuleGroup ,
ParentVersion : 0 ,
RestoredFrom : 0 ,
Version : 1 ,
Created : time . Now ( ) . UTC ( ) ,
Title : a . Title ,
Condition : a . Condition ,
Data : a . Data ,
IntervalSeconds : a . IntervalSeconds ,
NoDataState : a . NoDataState ,
ExecErrState : a . ExecErrState ,
For : a . For ,
Annotations : a . Annotations ,
Labels : map [ string ] string { } ,
}
2021-04-29 12:24:37 -05:00
}
2021-05-31 09:17:17 -05:00
func addMigrationInfo ( da * dashAlert ) ( map [ string ] string , map [ string ] string ) {
lbls := da . ParsedSettings . AlertRuleTags
if lbls == nil {
lbls = make ( map [ string ] string )
2021-05-03 10:42:31 -05:00
}
2021-05-31 09:17:17 -05:00
annotations := make ( map [ string ] string , 3 )
2021-10-07 16:30:06 -05:00
annotations [ ngmodels . DashboardUIDAnnotation ] = da . DashboardUID
annotations [ ngmodels . PanelIDAnnotation ] = fmt . Sprintf ( "%v" , da . PanelId )
2021-05-03 10:42:31 -05:00
annotations [ "__alertId__" ] = fmt . Sprintf ( "%v" , da . Id )
2021-05-31 09:17:17 -05:00
return lbls , annotations
2021-05-03 10:42:31 -05:00
}
2021-04-29 12:24:37 -05:00
func ( m * migration ) makeAlertRule ( cond condition , da dashAlert , folderUID string ) ( * alertRule , error ) {
2021-05-31 09:17:17 -05:00
lbls , annotations := addMigrationInfo ( & da )
lbls [ "alertname" ] = da . Name
annotations [ "message" ] = da . Message
2021-12-01 05:45:27 -06:00
var err error
data , err := migrateAlertRuleQueries ( cond . Data )
if err != nil {
return nil , fmt . Errorf ( "failed to migrate alert rule queries: %w" , err )
}
2021-04-29 12:24:37 -05:00
ar := & alertRule {
2021-08-12 08:04:09 -05:00
OrgID : da . OrgId ,
2021-04-29 12:24:37 -05:00
Title : da . Name , // TODO: Make sure all names are unique, make new name on constraint insert error.
2021-08-12 08:04:09 -05:00
UID : util . GenerateShortUID ( ) ,
2021-04-29 12:24:37 -05:00
Condition : cond . Condition ,
2021-12-01 05:45:27 -06:00
Data : data ,
2021-04-29 12:24:37 -05:00
IntervalSeconds : ruleAdjustInterval ( da . Frequency ) ,
2021-04-30 14:08:01 -05:00
Version : 1 ,
2021-08-12 08:04:09 -05:00
NamespaceUID : folderUID , // Folder already created, comes from env var.
2021-04-29 12:24:37 -05:00
RuleGroup : da . Name ,
For : duration ( da . For ) ,
Updated : time . Now ( ) . UTC ( ) ,
Annotations : annotations ,
2021-05-31 09:17:17 -05:00
Labels : lbls ,
2021-04-29 12:24:37 -05:00
}
ar . NoDataState , err = transNoData ( da . ParsedSettings . NoDataState )
if err != nil {
return nil , err
}
ar . ExecErrState , err = transExecErr ( da . ParsedSettings . ExecutionErrorState )
if err != nil {
return nil , err
}
2021-05-31 07:00:58 -05:00
// Label for routing and silences.
2021-08-12 08:04:09 -05:00
n , v := getLabelForRouteMatching ( ar . UID )
2021-05-31 07:00:58 -05:00
ar . Labels [ n ] = v
if err := m . addSilence ( da , ar ) ; err != nil {
m . mg . Logger . Error ( "alert migration error: failed to create silence" , "rule_name" , ar . Title , "err" , err )
}
2021-11-25 04:46:47 -06:00
if err := m . addErrorSilence ( da , ar ) ; err != nil {
m . mg . Logger . Error ( "alert migration error: failed to create silence for Error" , "rule_name" , ar . Title , "err" , err )
}
2021-11-04 15:42:34 -05:00
if err := m . addNoDataSilence ( da , ar ) ; err != nil {
m . mg . Logger . Error ( "alert migration error: failed to create silence for NoData" , "rule_name" , ar . Title , "err" , err )
}
2021-04-29 12:24:37 -05:00
return ar , nil
}
2021-12-01 05:45:27 -06:00
// migrateAlertRuleQueries attempts to fix alert rule queries so they can work in unified alerting. Queries of some data sources are not compatible with unified alerting.
func migrateAlertRuleQueries ( data [ ] alertQuery ) ( [ ] alertQuery , error ) {
result := make ( [ ] alertQuery , 0 , len ( data ) )
for _ , d := range data {
// queries that are expression are not relevant, skip them.
if d . DatasourceUID == expr . OldDatasourceUID {
result = append ( result , d )
continue
}
var fixedData map [ string ] json . RawMessage
err := json . Unmarshal ( d . Model , & fixedData )
if err != nil {
return nil , err
}
fixedData = fixGraphiteReferencedSubQueries ( fixedData )
updatedModel , err := json . Marshal ( fixedData )
if err != nil {
return nil , err
}
d . Model = updatedModel
result = append ( result , d )
}
return result , nil
}
// fixGraphiteReferencedSubQueries attempts to fix graphite referenced sub queries, given unified alerting does not support this.
// targetFull of Graphite data source contains the expanded version of field 'target', so let's copy that.
func fixGraphiteReferencedSubQueries ( queryData map [ string ] json . RawMessage ) map [ string ] json . RawMessage {
fullQuery , ok := queryData [ graphite . TargetFullModelField ]
if ok {
delete ( queryData , graphite . TargetFullModelField )
queryData [ graphite . TargetModelField ] = fullQuery
}
return queryData
}
2021-04-29 12:24:37 -05:00
type alertQuery struct {
// RefID is the unique identifier of the query, set by the frontend call.
RefID string ` json:"refId" `
// QueryType is an optional identifier for the type of query.
// It can be used to distinguish different types of queries.
QueryType string ` json:"queryType" `
// RelativeTimeRange is the relative Start and End of the query as sent by the frontend.
RelativeTimeRange relativeTimeRange ` json:"relativeTimeRange" `
DatasourceUID string ` json:"datasourceUid" `
// JSON is the raw JSON query and includes the above properties as well as custom properties.
Model json . RawMessage ` json:"model" `
}
// RelativeTimeRange is the per query start and end time
// for requests.
type relativeTimeRange struct {
From duration ` json:"from" `
To duration ` json:"to" `
}
// duration is a type used for marshalling durations.
type duration time . Duration
func ( d duration ) String ( ) string {
return time . Duration ( d ) . String ( )
}
func ( d duration ) MarshalJSON ( ) ( [ ] byte , error ) {
return json . Marshal ( time . Duration ( d ) . Seconds ( ) )
}
func ( d * duration ) UnmarshalJSON ( b [ ] byte ) error {
var v interface { }
if err := json . Unmarshal ( b , & v ) ; err != nil {
return err
}
switch value := v . ( type ) {
case float64 :
* d = duration ( time . Duration ( value ) * time . Second )
return nil
default :
return fmt . Errorf ( "invalid duration %v" , v )
}
}
func ruleAdjustInterval ( freq int64 ) int64 {
// 10 corresponds to the SchedulerCfg, but TODO not worrying about fetching for now.
var baseFreq int64 = 10
if freq <= baseFreq {
return 10
}
return freq - ( freq % baseFreq )
}
func transNoData ( s string ) ( string , error ) {
switch s {
case "ok" :
return "OK" , nil // values from ngalert/models/rule
case "" , "no_data" :
return "NoData" , nil
case "alerting" :
return "Alerting" , nil
case "keep_state" :
2021-11-04 15:42:34 -05:00
return "NoData" , nil // "keep last state" translates to no data because we now emit a special alert when the state is "noData". The result is that the evaluation will not return firing and instead we'll raise the special alert.
2021-04-29 12:24:37 -05:00
}
return "" , fmt . Errorf ( "unrecognized No Data setting %v" , s )
}
func transExecErr ( s string ) ( string , error ) {
switch s {
case "" , "alerting" :
return "Alerting" , nil
2021-05-18 12:55:43 -05:00
case "keep_state" :
2021-11-25 04:46:47 -06:00
// Keep last state is translated to error as we now emit a
// DatasourceError alert when the state is error
return "Error" , nil
2022-02-10 14:57:43 -06:00
case "ok" :
return "OK" , nil
2021-04-29 12:24:37 -05:00
}
return "" , fmt . Errorf ( "unrecognized Execution Error setting %v" , s )
}