From 2d4228fd82d0f0cfea91a8a51736c872f908f431 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Fri, 10 Feb 2017 08:47:08 -0600 Subject: [PATCH] Add handling of string values for kapacitor threshold alerts --- kapacitor/tickscripts_test.go | 274 ++++++++++++++++++++++++++++++++++ kapacitor/vars.go | 21 ++- kapacitor/vars_test.go | 50 +++++++ 3 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 kapacitor/vars_test.go diff --git a/kapacitor/tickscripts_test.go b/kapacitor/tickscripts_test.go index c00e043782..d201d44b89 100644 --- a/kapacitor/tickscripts_test.go +++ b/kapacitor/tickscripts_test.go @@ -199,6 +199,280 @@ trigger } } +func TestThresholdStringCrit(t *testing.T) { + alert := chronograf.AlertRule{ + Name: "haproxy", + Trigger: "threshold", + Alerts: []string{"email"}, + TriggerValues: chronograf.TriggerValues{ + Operator: "equal to", + Value: "DOWN", + }, + Every: "10s", + Message: `Haproxy monitor : {{.ID}} : {{ index .Tags "server" }} : {{ index .Tags "pxname" }} is {{ .Level }} `, + Details: "Email template", + Query: chronograf.QueryConfig{ + Database: "influxdb", + RetentionPolicy: "autogen", + Measurement: "haproxy", + Fields: []chronograf.Field{ + { + Field: "status", + Funcs: []string{"last"}, + }, + }, + GroupBy: chronograf.GroupBy{ + Time: "10s", + Tags: []string{"pxname"}, + }, + AreTagsAccepted: true, + }, + } + + tests := []struct { + name string + alert chronograf.AlertRule + want chronograf.TICKScript + wantErr bool + }{ + { + name: "Test valid template alert", + alert: alert, + want: `var db = 'influxdb' + +var rp = 'autogen' + +var measurement = 'haproxy' + +var groupBy = ['pxname'] + +var whereFilter = lambda: TRUE + +var period = 10s + +var every = 10s + +var name = 'haproxy' + +var idVar = name + ':{{.Group}}' + +var message = 'Haproxy monitor : {{.ID}} : {{ index .Tags "server" }} : {{ index .Tags "pxname" }} is {{ .Level }} ' + +var idTag = 'alertID' + +var levelTag = 'level' + +var messageField = 'message' + +var durationField = 'duration' + +var outputDB = 'chronograf' + +var outputRP = 'autogen' + +var outputMeasurement = 'alerts' + +var triggerType = 'threshold' + +var details = 'Email template' + +var crit = 'DOWN' + +var data = stream + |from() + .database(db) + .retentionPolicy(rp) + .measurement(measurement) + .groupBy(groupBy) + .where(whereFilter) + |window() + .period(period) + .every(every) + .align() + |last('status') + .as('value') + +var trigger = data + |alert() + .crit(lambda: "value" == crit) + .stateChangesOnly() + .message(message) + .id(idVar) + .idTag(idTag) + .levelTag(levelTag) + .messageField(messageField) + .durationField(durationField) + .details(details) + .email() + +trigger + |influxDBOut() + .create() + .database(outputDB) + .retentionPolicy(outputRP) + .measurement(outputMeasurement) + .tag('alertName', name) + .tag('triggerType', triggerType) + +trigger + |httpOut('output') +`, + wantErr: false, + }, + } + for _, tt := range tests { + gen := Alert{} + got, err := gen.Generate(tt.alert) + if (err != nil) != tt.wantErr { + t.Errorf("%q. Threshold() error = %v, wantErr %v", tt.name, err, tt.wantErr) + continue + } + if got != tt.want { + diff := diffmatchpatch.New() + delta := diff.DiffMain(string(tt.want), string(got), true) + t.Errorf("%q\n%s", tt.name, diff.DiffPrettyText(delta)) + } + } +} + +// TODO: Check with Nathaniel if kapacitor can do inequalities on strings +// If it cannot, I think we should add operator checks. +func TestThresholdStringCritGreater(t *testing.T) { + alert := chronograf.AlertRule{ + Name: "haproxy", + Trigger: "threshold", + Alerts: []string{"email"}, + TriggerValues: chronograf.TriggerValues{ + Operator: "greater than", + Value: "DOWN", + }, + Every: "10s", + Message: `Haproxy monitor : {{.ID}} : {{ index .Tags "server" }} : {{ index .Tags "pxname" }} is {{ .Level }} `, + Details: "Email template", + Query: chronograf.QueryConfig{ + Database: "influxdb", + RetentionPolicy: "autogen", + Measurement: "haproxy", + Fields: []chronograf.Field{ + { + Field: "status", + Funcs: []string{"last"}, + }, + }, + GroupBy: chronograf.GroupBy{ + Time: "10s", + Tags: []string{"pxname"}, + }, + AreTagsAccepted: true, + }, + } + + tests := []struct { + name string + alert chronograf.AlertRule + want chronograf.TICKScript + wantErr bool + }{ + { + name: "Test valid template alert", + alert: alert, + want: `var db = 'influxdb' + +var rp = 'autogen' + +var measurement = 'haproxy' + +var groupBy = ['pxname'] + +var whereFilter = lambda: TRUE + +var period = 10s + +var every = 10s + +var name = 'haproxy' + +var idVar = name + ':{{.Group}}' + +var message = 'Haproxy monitor : {{.ID}} : {{ index .Tags "server" }} : {{ index .Tags "pxname" }} is {{ .Level }} ' + +var idTag = 'alertID' + +var levelTag = 'level' + +var messageField = 'message' + +var durationField = 'duration' + +var outputDB = 'chronograf' + +var outputRP = 'autogen' + +var outputMeasurement = 'alerts' + +var triggerType = 'threshold' + +var details = 'Email template' + +var crit = 'DOWN' + +var data = stream + |from() + .database(db) + .retentionPolicy(rp) + .measurement(measurement) + .groupBy(groupBy) + .where(whereFilter) + |window() + .period(period) + .every(every) + .align() + |last('status') + .as('value') + +var trigger = data + |alert() + .crit(lambda: "value" > crit) + .stateChangesOnly() + .message(message) + .id(idVar) + .idTag(idTag) + .levelTag(levelTag) + .messageField(messageField) + .durationField(durationField) + .details(details) + .email() + +trigger + |influxDBOut() + .create() + .database(outputDB) + .retentionPolicy(outputRP) + .measurement(outputMeasurement) + .tag('alertName', name) + .tag('triggerType', triggerType) + +trigger + |httpOut('output') +`, + wantErr: false, + }, + } + for _, tt := range tests { + gen := Alert{} + got, err := gen.Generate(tt.alert) + if (err != nil) != tt.wantErr { + t.Errorf("%q. Threshold() error = %v, wantErr %v", tt.name, err, tt.wantErr) + continue + } + if got != tt.want { + diff := diffmatchpatch.New() + delta := diff.DiffMain(string(tt.want), string(got), true) + t.Errorf("%q\n%s", tt.name, diff.DiffPrettyText(delta)) + } + } +} + func TestThresholdDetail(t *testing.T) { alert := chronograf.AlertRule{ Name: "name", diff --git a/kapacitor/vars.go b/kapacitor/vars.go index f9239e6bfd..0c1cfd147b 100644 --- a/kapacitor/vars.go +++ b/kapacitor/vars.go @@ -3,6 +3,7 @@ package kapacitor import ( "fmt" "sort" + "strconv" "strings" "github.com/influxdata/chronograf" @@ -39,15 +40,16 @@ func Vars(rule chronograf.AlertRule) (string, error) { %s var crit = %s ` - return fmt.Sprintf(vars, - common, - rule.TriggerValues.Value), nil + // If critical value is a string, we'll + // need to single-quote it. + crit := critVar(rule.TriggerValues.Value) + return fmt.Sprintf(vars, common, crit), nil } else { vars := ` %s var lower = %s var upper = %s - ` +` return fmt.Sprintf(vars, common, rule.TriggerValues.Value, @@ -178,3 +180,14 @@ func whereFilter(q chronograf.QueryConfig) string { return "lambda: TRUE" } + +// critVar return the same string if a numeric type +// or if it is a string will return it as a kapacitor +// formatted single-quoted string +func critVar(value string) string { + // Test if numeric if it can be converted to a float + if _, err := strconv.ParseFloat(value, 64); err == nil { + return value + } + return "'" + value + "'" +} diff --git a/kapacitor/vars_test.go b/kapacitor/vars_test.go new file mode 100644 index 0000000000..e19104aeff --- /dev/null +++ b/kapacitor/vars_test.go @@ -0,0 +1,50 @@ +package kapacitor + +import ( + "fmt" + "testing" + + "github.com/influxdata/chronograf" +) + +func TestVarsCritStringEqual(t *testing.T) { + alert := chronograf.AlertRule{ + Name: "name", + Trigger: "threshold", + TriggerValues: chronograf.TriggerValues{ + Operator: "equal to", + Value: "DOWN", + }, + Every: "30s", + Query: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "haproxy", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + { + Field: "status", + }, + }, + GroupBy: chronograf.GroupBy{ + Time: "10m", + Tags: []string{"pxname"}, + }, + AreTagsAccepted: true, + }, + } + + raw, err := Vars(alert) + if err != nil { + fmt.Printf("%s", raw) + t.Fatalf("Error generating alert: %v %s", err, raw) + } + + tick, err := formatTick(raw) + if err != nil { + t.Errorf("Error formatting alert: %v %s", err, raw) + } + + if err := validateTick(tick); err != nil { + t.Errorf("Error validating alert: %v %s", err, tick) + } +}