influxdb/kapacitor/ast.go

915 lines
22 KiB
Go

package kapacitor
import (
"regexp"
"strconv"
"strings"
"time"
"github.com/influxdata/chronograf"
"github.com/influxdata/kapacitor/pipeline"
"github.com/influxdata/kapacitor/tick"
"github.com/influxdata/kapacitor/tick/ast"
"github.com/influxdata/kapacitor/tick/stateful"
)
func varString(kapaVar string, vars map[string]tick.Var) (string, bool) {
var ok bool
v, ok := vars[kapaVar]
if !ok {
return "", ok
}
strVar, ok := v.Value.(string)
return strVar, ok
}
func varValue(kapaVar string, vars map[string]tick.Var) (string, bool) {
var ok bool
v, ok := vars[kapaVar]
if !ok {
return "", ok
}
switch val := v.Value.(type) {
case string:
return val, true
case float64:
return strconv.FormatFloat(val, 'f', -1, 32), true
case int64:
return strconv.FormatInt(val, 10), true
case bool:
return strconv.FormatBool(val), true
case time.Time:
return val.String(), true
case *regexp.Regexp:
return val.String(), true
default:
return "", false
}
}
func varDuration(kapaVar string, vars map[string]tick.Var) (string, bool) {
var ok bool
v, ok := vars[kapaVar]
if !ok {
return "", ok
}
durVar, ok := v.Value.(time.Duration)
if !ok {
return "", ok
}
return durVar.String(), true
}
func varStringList(kapaVar string, vars map[string]tick.Var) ([]string, bool) {
v, ok := vars[kapaVar]
if !ok {
return nil, ok
}
list, ok := v.Value.([]tick.Var)
if !ok {
return nil, ok
}
strs := make([]string, len(list))
for i, l := range list {
s, ok := l.Value.(string)
if !ok {
return nil, ok
}
strs[i] = s
}
return strs, ok
}
// WhereFilter filters the stream data in a TICKScript
type WhereFilter struct {
TagValues map[string][]string // Tags are filtered by an array of values
Operator string // Operator is == or !=
}
func varWhereFilter(vars map[string]tick.Var) (WhereFilter, bool) {
// All chronograf TICKScripts have whereFilters.
v, ok := vars["whereFilter"]
if !ok {
return WhereFilter{}, ok
}
filter := WhereFilter{}
filter.TagValues = make(map[string][]string)
// All chronograf TICKScript's whereFilter use a lambda function.
value, ok := v.Value.(*ast.LambdaNode)
if !ok {
return WhereFilter{}, ok
}
lambda := value.ExpressionString()
// Chronograf TICKScripts use lambda: TRUE as a pass-throug where clause
// if the script does not have a where clause set.
if lambda == "TRUE" {
return WhereFilter{}, true
}
opSet := map[string]struct{}{} // All ops must be the same b/c queryConfig
// Otherwise the lambda function will be several "tag" op 'value' expressions.
var re = regexp.MustCompile(`(?U)"(.*)"\s+(==|!=)\s+'(.*)'`)
for _, match := range re.FindAllStringSubmatch(lambda, -1) {
tag, op, value := match[1], match[2], match[3]
opSet[op] = struct{}{}
values, ok := filter.TagValues[tag]
if !ok {
values = make([]string, 0)
}
values = append(values, value)
filter.TagValues[tag] = values
}
// An obscure piece of the queryConfig is that the operator in ALL binary
// expressions just be the same. So, there must only be one operator
// in our opSet
if len(opSet) != 1 {
return WhereFilter{}, false
}
for op := range opSet {
if op != "==" && op != "!=" {
return WhereFilter{}, false
}
filter.Operator = op
}
return filter, true
}
// CommonVars includes all the variables of a chronograf TICKScript
type CommonVars struct {
DB string
RP string
Measurement string
Name string
Message string
TriggerType string
GroupBy []string
Filter WhereFilter
Period string
Every string
Detail string
}
// ThresholdVars represents the critical value where an alert occurs
type ThresholdVars struct {
Crit string
}
// RangeVars represents the critical range where an alert occurs
type RangeVars struct {
Lower string
Upper string
}
// RelativeVars represents the critical range and time in the past an alert occurs
type RelativeVars struct {
Shift string
Crit string
}
// DeadmanVars represents a deadman alert
type DeadmanVars struct{}
func extractCommonVars(vars map[string]tick.Var) (CommonVars, error) {
res := CommonVars{}
// All these variables must exist to be a chronograf TICKScript
// If any of these don't exist, then this isn't a tickscript we can process
var ok bool
res.DB, ok = varString("db", vars)
if !ok {
return CommonVars{}, ErrNotChronoTickscript
}
res.RP, ok = varString("rp", vars)
if !ok {
return CommonVars{}, ErrNotChronoTickscript
}
res.Measurement, ok = varString("measurement", vars)
if !ok {
return CommonVars{}, ErrNotChronoTickscript
}
res.Name, ok = varString("name", vars)
if !ok {
return CommonVars{}, ErrNotChronoTickscript
}
res.Message, ok = varString("message", vars)
if !ok {
return CommonVars{}, ErrNotChronoTickscript
}
res.TriggerType, ok = varString("triggerType", vars)
if !ok {
return CommonVars{}, ErrNotChronoTickscript
}
// All chronograf TICKScripts have groupBy. Possible to be empty list though.
groups, ok := varStringList("groupBy", vars)
if !ok {
return CommonVars{}, ErrNotChronoTickscript
}
res.GroupBy = groups
// All chronograf TICKScripts must have a whereFitler. Could be empty.
res.Filter, ok = varWhereFilter(vars)
if !ok {
return CommonVars{}, ErrNotChronoTickscript
}
// Some chronograf TICKScripts have details associated with the alert.
// Typically, this is the body of an email alert.
if detail, ok := varString("details", vars); ok {
res.Detail = detail
}
// Relative and Threshold alerts may have an every variables
if every, ok := varDuration("every", vars); ok {
res.Every = every
}
// All alert types may have a period variables
if period, ok := varDuration("period", vars); ok {
res.Period = period
}
return res, nil
}
func extractAlertVars(vars map[string]tick.Var) (interface{}, error) {
// Depending on the type of the alert the variables set will be different
alertType, ok := varString("triggerType", vars)
if !ok {
return nil, ErrNotChronoTickscript
}
switch alertType {
case Deadman:
return &DeadmanVars{}, nil
case Threshold:
if crit, ok := varValue("crit", vars); ok {
return &ThresholdVars{
Crit: crit,
}, nil
}
r := &RangeVars{}
// Threshold Range alerts must have both an upper and lower bound
if r.Lower, ok = varValue("lower", vars); !ok {
return nil, ErrNotChronoTickscript
}
if r.Upper, ok = varValue("upper", vars); !ok {
return nil, ErrNotChronoTickscript
}
return r, nil
case Relative:
// Relative alerts must have a time shift and critical value
r := &RelativeVars{}
if r.Shift, ok = varDuration("shift", vars); !ok {
return nil, ErrNotChronoTickscript
}
if r.Crit, ok = varValue("crit", vars); !ok {
return nil, ErrNotChronoTickscript
}
return r, nil
default:
return nil, ErrNotChronoTickscript
}
}
// FieldFunc represents the field used as the alert value and its optional aggregate function
type FieldFunc struct {
Field string
Func string
}
func extractFieldFunc(script chronograf.TICKScript) FieldFunc {
// If the TICKScript is relative or threshold alert with an aggregate
// then the aggregate function and field is in the form |func('field').as('value')
var re = regexp.MustCompile(`(?Um)\|(\w+)\('(.*)'\)\s*\.as\('value'\)`)
for _, match := range re.FindAllStringSubmatch(string(script), -1) {
fn, field := match[1], match[2]
return FieldFunc{
Field: field,
Func: fn,
}
}
// If the alert does not have an aggregate then the the value function will
// be this form: |eval(lambda: "%s").as('value')
re = regexp.MustCompile(`(?Um)\|eval\(lambda: "(.*)"\)\s*\.as\('value'\)`)
for _, match := range re.FindAllStringSubmatch(string(script), -1) {
field := match[1]
return FieldFunc{
Field: field,
}
}
// Otherwise, if this could be a deadman alert and not have a FieldFunc
return FieldFunc{}
}
// CritCondition represents the operators that determine when the alert should go critical
type CritCondition struct {
Operators []string
}
func extractCrit(script chronograf.TICKScript) CritCondition {
// Threshold and relative alerts have the form .crit(lambda: "value" op crit)
// Threshold range alerts have the form .crit(lambda: "value" op lower op "value" op upper)
var re = regexp.MustCompile(`(?Um)\.crit\(lambda:\s+"value"\s+(.*)\s+crit\)`)
for _, match := range re.FindAllStringSubmatch(string(script), -1) {
op := match[1]
return CritCondition{
Operators: []string{
op,
},
}
}
re = regexp.MustCompile(`(?Um)\.crit\(lambda:\s+"value"\s+(.*)\s+lower\s+(.*)\s+"value"\s+(.*)\s+upper\)`)
for _, match := range re.FindAllStringSubmatch(string(script), -1) {
lower, compound, upper := match[1], match[2], match[3]
return CritCondition{
Operators: []string{
lower,
compound,
upper,
},
}
}
// It's possible to not have a critical condition if this is
// a deadman alert
return CritCondition{}
}
// alertType reads the TICKscript and returns the specific
// alerting type. If it is unable to determine it will
// return ErrNotChronoTickscript
func alertType(script chronograf.TICKScript) (string, error) {
t := string(script)
if strings.Contains(t, `var triggerType = 'threshold'`) {
if strings.Contains(t, `var crit = `) {
return Threshold, nil
} else if strings.Contains(t, `var lower = `) && strings.Contains(t, `var upper = `) {
return ThresholdRange, nil
}
return "", ErrNotChronoTickscript
} else if strings.Contains(t, `var triggerType = 'relative'`) {
if strings.Contains(t, `eval(lambda: float("current.value" - "past.value"))`) {
return ChangeAmount, nil
} else if strings.Contains(t, `|eval(lambda: abs(float("current.value" - "past.value")) / float("past.value") * 100.0)`) {
return ChangePercent, nil
}
return "", ErrNotChronoTickscript
} else if strings.Contains(t, `var triggerType = 'deadman'`) {
return Deadman, nil
}
return "", ErrNotChronoTickscript
}
// Reverse converts tickscript to an AlertRule
func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) {
rule := chronograf.AlertRule{
Alerts: []string{},
Query: &chronograf.QueryConfig{},
}
t, err := alertType(script)
if err != nil {
return rule, err
}
scope := stateful.NewScope()
template, err := pipeline.CreateTemplatePipeline(string(script), pipeline.StreamEdge, scope, &deadman{})
if err != nil {
return chronograf.AlertRule{}, err
}
vars := template.Vars()
commonVars, err := extractCommonVars(vars)
if err != nil {
return rule, err
}
alertVars, err := extractAlertVars(vars)
if err != nil {
return rule, err
}
fieldFunc := extractFieldFunc(script)
critCond := extractCrit(script)
switch t {
case Threshold, ChangeAmount, ChangePercent:
if len(critCond.Operators) != 1 {
return rule, ErrNotChronoTickscript
}
case ThresholdRange:
if len(critCond.Operators) != 3 {
return rule, ErrNotChronoTickscript
}
}
rule.Name = commonVars.Name
rule.Trigger = commonVars.TriggerType
rule.Message = commonVars.Message
rule.Details = commonVars.Detail
rule.Query.Database = commonVars.DB
rule.Query.RetentionPolicy = commonVars.RP
rule.Query.Measurement = commonVars.Measurement
rule.Query.GroupBy.Tags = commonVars.GroupBy
if commonVars.Filter.Operator == "==" {
rule.Query.AreTagsAccepted = true
}
rule.Query.Tags = commonVars.Filter.TagValues
if t == Deadman {
rule.TriggerValues.Period = commonVars.Period
} else {
rule.Query.GroupBy.Time = commonVars.Period
rule.Every = commonVars.Every
if fieldFunc.Func != "" {
rule.Query.Fields = []chronograf.Field{
{
Type: "func",
Value: fieldFunc.Func,
Args: []chronograf.Field{
{
Value: fieldFunc.Field,
Type: "field",
},
},
},
}
} else {
rule.Query.Fields = []chronograf.Field{
{
Type: "field",
Value: fieldFunc.Field,
},
}
}
}
switch t {
case ChangeAmount, ChangePercent:
rule.TriggerValues.Change = t
rule.TriggerValues.Operator, err = chronoOperator(critCond.Operators[0])
if err != nil {
return rule, ErrNotChronoTickscript
}
v, ok := alertVars.(*RelativeVars)
if !ok {
return rule, ErrNotChronoTickscript
}
rule.TriggerValues.Value = v.Crit
rule.TriggerValues.Shift = v.Shift
case Threshold:
rule.TriggerValues.Operator, err = chronoOperator(critCond.Operators[0])
if err != nil {
return rule, ErrNotChronoTickscript
}
v, ok := alertVars.(*ThresholdVars)
if !ok {
return rule, ErrNotChronoTickscript
}
rule.TriggerValues.Value = v.Crit
case ThresholdRange:
rule.TriggerValues.Operator, err = chronoRangeOperators(critCond.Operators)
v, ok := alertVars.(*RangeVars)
if !ok {
return rule, ErrNotChronoTickscript
}
rule.TriggerValues.Value = v.Lower
rule.TriggerValues.RangeValue = v.Upper
}
p, err := pipeline.CreatePipeline(string(script), pipeline.StreamEdge, stateful.NewScope(), &deadman{}, vars)
if err != nil {
return chronograf.AlertRule{}, err
}
extractAlertNodes(p, &rule)
return rule, err
}
func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) {
p.Walk(func(n pipeline.Node) error {
switch t := n.(type) {
case *pipeline.AlertNode:
extractHipchat(t, rule)
extractOpsgenie(t, rule)
extractPagerduty(t, rule)
extractVictorops(t, rule)
extractEmail(t, rule)
extractPost(t, rule)
extractAlerta(t, rule)
extractSensu(t, rule)
extractSlack(t, rule)
extractTalk(t, rule)
extractTelegram(t, rule)
extractPushover(t, rule)
extractTCP(t, rule)
extractLog(t, rule)
extractExec(t, rule)
}
return nil
})
}
func extractHipchat(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.HipChatHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "hipchat")
h := node.HipChatHandlers[0]
alert := chronograf.KapacitorNode{
Name: "hipchat",
}
if h.Room != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "room",
Args: []string{h.Room},
})
}
if h.Token != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "token",
Args: []string{h.Token},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractOpsgenie(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.OpsGenieHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "opsgenie")
o := node.OpsGenieHandlers[0]
alert := chronograf.KapacitorNode{
Name: "opsgenie",
}
if len(o.RecipientsList) != 0 {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "recipients",
Args: o.RecipientsList,
})
}
if len(o.TeamsList) != 0 {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "teams",
Args: o.TeamsList,
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractPagerduty(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.PagerDutyHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "pagerduty")
p := node.PagerDutyHandlers[0]
alert := chronograf.KapacitorNode{
Name: "pagerduty",
}
if p.ServiceKey != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "serviceKey",
Args: []string{p.ServiceKey},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractVictorops(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.VictorOpsHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "victorops")
v := node.VictorOpsHandlers[0]
alert := chronograf.KapacitorNode{
Name: "victorops",
}
if v.RoutingKey != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "routingKey",
Args: []string{v.RoutingKey},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractEmail(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.EmailHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "smtp")
e := node.EmailHandlers[0]
alert := chronograf.KapacitorNode{
Name: "smtp",
}
if len(e.ToList) != 0 {
alert.Args = e.ToList
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractPost(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.HTTPPostHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "http")
p := node.HTTPPostHandlers[0]
alert := chronograf.KapacitorNode{
Name: "http",
}
if p.URL != "" {
alert.Args = []string{p.URL}
}
if p.Endpoint != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "endpoint",
Args: []string{p.Endpoint},
})
}
if len(p.Headers) > 0 {
for k, v := range p.Headers {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "header",
Args: []string{k, v},
})
}
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractAlerta(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.AlertaHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "alerta")
a := node.AlertaHandlers[0]
alert := chronograf.KapacitorNode{
Name: "alerta",
}
if a.Token != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "token",
Args: []string{a.Token},
})
}
if a.Resource != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "resource",
Args: []string{a.Resource},
})
}
if a.Event != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "event",
Args: []string{a.Event},
})
}
if a.Environment != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "environment",
Args: []string{a.Environment},
})
}
if a.Group != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "group",
Args: []string{a.Group},
})
}
if a.Value != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "value",
Args: []string{a.Value},
})
}
if a.Origin != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "origin",
Args: []string{a.Origin},
})
}
if a.Service != nil {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "services",
Args: a.Service,
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractSensu(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.SensuHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "sensu")
alert := chronograf.KapacitorNode{
Name: "sensu",
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractSlack(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.SlackHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "slack")
s := node.SlackHandlers[0]
alert := chronograf.KapacitorNode{
Name: "slack",
}
if s.Channel != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "channel",
Args: []string{s.Channel},
})
}
if s.IconEmoji != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "iconEmoji",
Args: []string{s.IconEmoji},
})
}
if s.Username != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "username",
Args: []string{s.Username},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractTalk(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.TalkHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "talk")
alert := chronograf.KapacitorNode{
Name: "talk",
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractTelegram(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.TelegramHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "telegram")
t := node.TelegramHandlers[0]
alert := chronograf.KapacitorNode{
Name: "telegram",
}
if t.ChatId != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "chatId",
Args: []string{t.ChatId},
})
}
if t.ParseMode != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "parseMode",
Args: []string{t.ParseMode},
})
}
if t.IsDisableWebPagePreview {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "disableWebPagePreview",
})
}
if t.IsDisableNotification {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "disableNotification",
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractTCP(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.TcpHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "tcp")
t := node.TcpHandlers[0]
alert := chronograf.KapacitorNode{
Name: "tcp",
}
if t.Address != "" {
alert.Args = []string{t.Address}
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractLog(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.LogHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "log")
log := node.LogHandlers[0]
alert := chronograf.KapacitorNode{
Name: "log",
}
if log.FilePath != "" {
alert.Args = []string{log.FilePath}
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractExec(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.ExecHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "exec")
exec := node.ExecHandlers[0]
alert := chronograf.KapacitorNode{
Name: "exec",
}
if len(exec.Command) != 0 {
alert.Args = exec.Command
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractPushover(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.PushoverHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "pushover")
a := node.PushoverHandlers[0]
alert := chronograf.KapacitorNode{
Name: "pushover",
}
if a.Device != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "device",
Args: []string{a.Device},
})
}
if a.Title != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "title",
Args: []string{a.Title},
})
}
if a.URL != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "URL",
Args: []string{a.URL},
})
}
if a.URLTitle != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "URLTitle",
Args: []string{a.URLTitle},
})
}
if a.Sound != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "sound",
Args: []string{a.Sound},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}