Add full kapacitor ast to chronograf.AlertRule with tests

pull/10616/head
Chris Goller 2017-04-03 23:11:18 -05:00
parent cbc5ee5e13
commit a449a74c72
2 changed files with 379 additions and 109 deletions

View File

@ -3,6 +3,7 @@ package kapacitor
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
@ -23,6 +24,43 @@ func varString(kapaVar string, vars map[string]tick.Var) (string, bool) {
return strVar, ok 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) { func varStringList(kapaVar string, vars map[string]tick.Var) ([]string, bool) {
v, ok := vars[kapaVar] v, ok := vars[kapaVar]
if !ok { if !ok {
@ -93,6 +131,9 @@ func varWhereFilter(vars map[string]tick.Var) (WhereFilter, bool) {
return WhereFilter{}, false return WhereFilter{}, false
} }
for op := range opSet { for op := range opSet {
if op != "==" && op != "!=" {
return WhereFilter{}, false
}
filter.Operator = op filter.Operator = op
} }
return filter, true return filter, true
@ -100,56 +141,67 @@ func varWhereFilter(vars map[string]tick.Var) (WhereFilter, bool) {
// CommonVars includes all the variables of a chronograf TICKScript // CommonVars includes all the variables of a chronograf TICKScript
type CommonVars struct { type CommonVars struct {
Vars map[string]string DB string
GroupBy []string RP string
Filter WhereFilter Measurement string
Period string Name string
Every string Message string
Detail 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 { type ThresholdVars struct {
Crit string Crit string
} }
// RangeVars represents the critical range where an alert occurs
type RangeVars struct { type RangeVars struct {
Lower string Lower string
Upper string Upper string
} }
// RelativeVars represents the critical range and time in the past an alert occurs
type RelativeVars struct { type RelativeVars struct {
Shift string Shift string
Crit string Crit string
} }
// DeadmanVars represents a deadman alert
type DeadmanVars struct{} type DeadmanVars struct{}
func extractCommonVars(script chronograf.TICKScript) (CommonVars, error) { func extractCommonVars(vars map[string]tick.Var) (CommonVars, error) {
scope := stateful.NewScope()
template, err := pipeline.CreateTemplatePipeline(string(script), pipeline.StreamEdge, scope, &deadman{})
if err != nil {
return CommonVars{}, err
}
vars := template.Vars()
res := CommonVars{} res := CommonVars{}
// All these variables must exist to be a chronograf TICKScript // All these variables must exist to be a chronograf TICKScript
commonStrs := []string{ // If any of these don't exist, then this isn't a tickscript we can process
"db", var ok bool
"rp", res.DB, ok = varString("db", vars)
"measurement", if !ok {
"name", return CommonVars{}, ErrNotChronoTickscript
"message",
"triggerType",
} }
res.RP, ok = varString("rp", vars)
for _, v := range commonStrs { if !ok {
str, ok := varString(v, vars) return CommonVars{}, ErrNotChronoTickscript
// Didn't exist so, this isn't a tickscript we can process }
if !ok { res.Measurement, ok = varString("measurement", vars)
return CommonVars{}, ErrNotChronoTickscript if !ok {
} return CommonVars{}, ErrNotChronoTickscript
res.Vars[v] = str }
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. // All chronograf TICKScripts have groupBy. Possible to be empty list though.
@ -172,12 +224,12 @@ func extractCommonVars(script chronograf.TICKScript) (CommonVars, error) {
} }
// Relative and Threshold alerts may have an every variables // Relative and Threshold alerts may have an every variables
if every, ok := varString("every", vars); ok { if every, ok := varDuration("every", vars); ok {
res.Every = every res.Every = every
} }
// All alert types may have a period variables // All alert types may have a period variables
if period, ok := varString("period", vars); ok { if period, ok := varDuration("period", vars); ok {
res.Period = period res.Period = period
} }
return res, nil return res, nil
@ -194,27 +246,28 @@ func extractAlertVars(vars map[string]tick.Var) (interface{}, error) {
case Deadman: case Deadman:
return &DeadmanVars{}, nil return &DeadmanVars{}, nil
case Threshold: case Threshold:
if crit, ok := varString("crit", vars); ok { if crit, ok := varValue("crit", vars); ok {
return &ThresholdVars{ return &ThresholdVars{
Crit: crit, Crit: crit,
}, nil }, nil
} }
r := &RangeVars{} r := &RangeVars{}
// Threshold Range alerts must have both an upper and lower bound // Threshold Range alerts must have both an upper and lower bound
if r.Lower, ok = varString("lower", vars); !ok { if r.Lower, ok = varValue("lower", vars); !ok {
return nil, ErrNotChronoTickscript return nil, ErrNotChronoTickscript
} }
if r.Upper, ok = varString("upper", vars); !ok { if r.Upper, ok = varValue("upper", vars); !ok {
return nil, ErrNotChronoTickscript return nil, ErrNotChronoTickscript
} }
return r, nil return r, nil
case Relative: case Relative:
// Relative alerts must have a time shift and critical value // Relative alerts must have a time shift and critical value
r := &RelativeVars{} r := &RelativeVars{}
if r.Shift, ok = varString("shift", vars); !ok { if r.Shift, ok = varDuration("shift", vars); !ok {
return nil, ErrNotChronoTickscript return nil, ErrNotChronoTickscript
} }
if r.Crit, ok = varString("crit", vars); !ok { // TODO: crit could be string, float, int
if r.Crit, ok = varValue("crit", vars); !ok {
return nil, ErrNotChronoTickscript return nil, ErrNotChronoTickscript
} }
return r, nil return r, nil
@ -254,6 +307,7 @@ func extractFieldFunc(script chronograf.TICKScript) FieldFunc {
return FieldFunc{} return FieldFunc{}
} }
// CritCondition represents the operators that determine when the alert should go critical
type CritCondition struct { type CritCondition struct {
Operators []string Operators []string
} }
@ -317,12 +371,106 @@ func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) {
rule := chronograf.AlertRule{ rule := chronograf.AlertRule{
Alerts: []string{}, Alerts: []string{},
} }
t, err := alertType(script)
if err != nil {
return rule, err
}
scope := stateful.NewScope() scope := stateful.NewScope()
template, err := pipeline.CreateTemplatePipeline(string(script), pipeline.StreamEdge, scope, &deadman{}) template, err := pipeline.CreateTemplatePipeline(string(script), pipeline.StreamEdge, scope, &deadman{})
if err != nil { if err != nil {
return chronograf.AlertRule{}, err return chronograf.AlertRule{}, err
} }
vars := template.Vars() 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.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
rule.Query.Fields = []chronograf.Field{
{
Field: fieldFunc.Field,
Funcs: []string{fieldFunc.Func},
},
}
}
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)
if err := valueStr("db", &rule.Query.Database, vars); err != nil { if err := valueStr("db", &rule.Query.Database, vars); err != nil {
return chronograf.AlertRule{}, err return chronograf.AlertRule{}, err
} }
@ -382,56 +530,6 @@ func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) {
} }
} }
p, err := pipeline.CreatePipeline(string(script), pipeline.StreamEdge, stateful.NewScope(), &deadman{}, vars)
if err != nil {
return chronograf.AlertRule{}, err
}
p.Walk(func(n pipeline.Node) error {
switch t := n.(type) {
case *pipeline.AlertNode:
if t.HipChatHandlers != nil {
}
if t.OpsGenieHandlers != nil {
rule.Alerts = append(rule.Alerts, "hipchat")
}
if t.PagerDutyHandlers != nil {
rule.Alerts = append(rule.Alerts, "pagerduty")
}
if t.VictorOpsHandlers != nil {
rule.Alerts = append(rule.Alerts, "victorops")
}
if t.EmailHandlers != nil {
rule.Alerts = append(rule.Alerts, "smtp")
}
if t.PostHandlers != nil {
rule.Alerts = append(rule.Alerts, "http")
}
if t.AlertaHandlers != nil {
rule.Alerts = append(rule.Alerts, "alerta")
}
if t.SensuHandlers != nil {
rule.Alerts = append(rule.Alerts, "sensu")
}
if t.SlackHandlers != nil {
rule.Alerts = append(rule.Alerts, "slack")
}
if t.TalkHandlers != nil {
rule.Alerts = append(rule.Alerts, "talk")
}
if t.TelegramHandlers != nil {
rule.Alerts = append(rule.Alerts, "telegram")
}
if t.TcpHandlers != nil {
rule.Alerts = append(rule.Alerts, "tcp")
}
if t.ExecHandlers != nil {
rule.Alerts = append(rule.Alerts, "exec")
}
}
return nil
})
return rule, err return rule, err
} }
@ -448,6 +546,28 @@ func valueStr(key string, value *string, vars map[string]tick.Var) error {
return nil return nil
} }
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)
extractTCP(t, rule)
extractExec(t, rule)
}
return nil
})
}
func extractHipchat(node *pipeline.AlertNode, rule *chronograf.AlertRule) { func extractHipchat(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.HipChatHandlers == nil { if node.HipChatHandlers == nil {
return return
@ -455,8 +575,7 @@ func extractHipchat(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.Alerts = append(rule.Alerts, "hipchat") rule.Alerts = append(rule.Alerts, "hipchat")
h := node.HipChatHandlers[0] h := node.HipChatHandlers[0]
alert := chronograf.KapacitorNode{ alert := chronograf.KapacitorNode{
Name: "hipchat", Name: "hipchat",
Properties: []chronograf.KapacitorProperty{},
} }
if h.Room != "" { if h.Room != "" {
@ -482,18 +601,17 @@ func extractOpsgenie(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.Alerts = append(rule.Alerts, "opsgenie") rule.Alerts = append(rule.Alerts, "opsgenie")
o := node.OpsGenieHandlers[0] o := node.OpsGenieHandlers[0]
alert := chronograf.KapacitorNode{ alert := chronograf.KapacitorNode{
Name: "opsgenie", Name: "opsgenie",
Properties: []chronograf.KapacitorProperty{},
} }
if o.RecipientsList != nil { if len(o.RecipientsList) != 0 {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{ alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "recipients", Name: "recipients",
Args: o.RecipientsList, Args: o.RecipientsList,
}) })
} }
if o.TeamsList != nil { if len(o.TeamsList) != 0 {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{ alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "teams", Name: "teams",
Args: o.TeamsList, Args: o.TeamsList,
@ -509,8 +627,7 @@ func extractPagerduty(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.Alerts = append(rule.Alerts, "pagerduty") rule.Alerts = append(rule.Alerts, "pagerduty")
p := node.PagerDutyHandlers[0] p := node.PagerDutyHandlers[0]
alert := chronograf.KapacitorNode{ alert := chronograf.KapacitorNode{
Name: "paperduty", Name: "paperduty",
Properties: []chronograf.KapacitorProperty{},
} }
if p.ServiceKey != "" { if p.ServiceKey != "" {
@ -529,8 +646,7 @@ func extractVictorops(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.Alerts = append(rule.Alerts, "victorops") rule.Alerts = append(rule.Alerts, "victorops")
v := node.VictorOpsHandlers[0] v := node.VictorOpsHandlers[0]
alert := chronograf.KapacitorNode{ alert := chronograf.KapacitorNode{
Name: "victorops", Name: "victorops",
Properties: []chronograf.KapacitorProperty{},
} }
if v.RoutingKey != "" { if v.RoutingKey != "" {
@ -549,11 +665,10 @@ func extractEmail(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.Alerts = append(rule.Alerts, "smtp") rule.Alerts = append(rule.Alerts, "smtp")
e := node.EmailHandlers[0] e := node.EmailHandlers[0]
alert := chronograf.KapacitorNode{ alert := chronograf.KapacitorNode{
Name: "smtp", Name: "smtp",
Properties: []chronograf.KapacitorProperty{},
} }
if e.ToList != nil { if len(e.ToList) != 0 {
alert.Args = e.ToList alert.Args = e.ToList
} }
rule.AlertNodes = append(rule.AlertNodes, alert) rule.AlertNodes = append(rule.AlertNodes, alert)
@ -567,7 +682,10 @@ func extractPost(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
p := node.PostHandlers[0] p := node.PostHandlers[0]
alert := chronograf.KapacitorNode{ alert := chronograf.KapacitorNode{
Name: "http", Name: "http",
Args: []string{p.URL}, }
if p.URL != "" {
alert.Args = []string{p.URL}
} }
rule.AlertNodes = append(rule.AlertNodes, alert) rule.AlertNodes = append(rule.AlertNodes, alert)
@ -580,8 +698,7 @@ func extractAlerta(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.Alerts = append(rule.Alerts, "alerta") rule.Alerts = append(rule.Alerts, "alerta")
a := node.AlertaHandlers[0] a := node.AlertaHandlers[0]
alert := chronograf.KapacitorNode{ alert := chronograf.KapacitorNode{
Name: "alerta", Name: "alerta",
Properties: []chronograf.KapacitorProperty{},
} }
if a.Token != "" { if a.Token != "" {
@ -662,15 +779,110 @@ func extractSlack(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.Alerts = append(rule.Alerts, "slack") rule.Alerts = append(rule.Alerts, "slack")
s := node.SlackHandlers[0] s := node.SlackHandlers[0]
alert := chronograf.KapacitorNode{ alert := chronograf.KapacitorNode{
Name: "slack", Name: "slack",
Properties: []chronograf.KapacitorProperty{},
} }
if a.Token != "" { if s.Channel != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{ alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "token", Name: "channel",
Args: []string{a.Token}, 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) rule.AlertNodes = append(rule.AlertNodes, alert)
} }
func extractTalk(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.TalkHandlers == nil {
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 node.TelegramHandlers == nil {
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 node.TcpHandlers == nil {
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 extractExec(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.ExecHandlers == nil {
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)
}

View File

@ -58,13 +58,25 @@ func TestReverse(t *testing.T) {
.durationField(durationField) .durationField(durationField)
.slack() .slack()
.victorOps() .victorOps()
.email() .email('howdy@howdy.com')
`), `),
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"victorops", "slack", "email"}, Alerts: []string{"victorops", "smtp", "slack"},
AlertNodes: []chronograf.KapacitorNode{
{
Name: "victorops",
},
{
Name: "smtp",
Args: []string{"howdy@howdy.com"},
},
{
Name: "slack",
},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
Value: "90", Value: "90",
@ -180,6 +192,52 @@ trigger
trigger trigger
|httpOut('output')`, |httpOut('output')`,
want: chronograf.AlertRule{
Query: chronograf.QueryConfig{
Database: "telegraf",
Measurement: "cpu",
RetentionPolicy: "autogen",
Fields: []chronograf.Field{
chronograf.Field{
Field: "usage_user",
Funcs: []string{"mean"},
},
},
Tags: map[string][]string{
"cpu": []string{"cpu_total"},
"host": []string{"acc-0eabc309-eu-west-1-data-3", "prod"},
},
GroupBy: chronograf.GroupBy{
Time: "10m0s",
Tags: []string{"host", "cluster_id"},
},
AreTagsAccepted: true,
},
Every: "30s",
Alerts: []string{
"victorops",
"smtp",
"slack",
},
AlertNodes: []chronograf.KapacitorNode{
chronograf.KapacitorNode{
Name: "victorops",
},
chronograf.KapacitorNode{
Name: "smtp",
},
chronograf.KapacitorNode{
Name: "slack",
},
},
Message: "message",
Trigger: "threshold",
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
Value: "90",
},
Name: "name",
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {