diff --git a/chronograf.go b/chronograf.go index 4cc6c87965..0b8d2a4170 100644 --- a/chronograf.go +++ b/chronograf.go @@ -139,13 +139,12 @@ type Ticker interface { // TriggerValues specifies the alerting logic for a specific trigger type type TriggerValues struct { - Change string `json:"change,omitempty"` // Change specifies if the change is a percent or absolute - Period string `json:"period,omitempty"` // Period length of time before deadman is alerted - Shift string `json:"shift,omitempty"` // Shift is the amount of time to look into the past for the alert to compare to the present - Operator string `json:"operator,omitempty"` // Operator for alert comparison - RangeOperator string `json:"range_operator,omitempty"` // RangeOperator is an optional operator for range comparisons - Value string `json:"value,omitempty"` // Value is the boundary value when alert goes critical - RangeValue string `json:"range_value,omitempty"` // RangeValue is an optional value for range comparisons + Change string `json:"change,omitempty"` // Change specifies if the change is a percent or absolute + Period string `json:"period,omitempty"` // Period length of time before deadman is alerted + Shift string `json:"shift,omitempty"` // Shift is the amount of time to look into the past for the alert to compare to the present + Operator string `json:"operator,omitempty"` // Operator for alert comparison + Value string `json:"value,omitempty"` // Value is the boundary value when alert goes critical + RangeValue string `json:"rangeValue,omitempty"` // RangeValue is an optional value for range comparisons } // Field represent influxql fields and functions from the UI diff --git a/kapacitor/operators.go b/kapacitor/operators.go index 22b05a93f4..9ca8ad1461 100644 --- a/kapacitor/operators.go +++ b/kapacitor/operators.go @@ -9,6 +9,8 @@ const ( GreaterThanEqual = "equal to or greater" Equal = "equal to" NotEqual = "not equal to" + InsideRange = "is inside range" + OutsideRange = "is outside range" ) // kapaOperator converts UI strings to kapacitor operators @@ -30,3 +32,14 @@ func kapaOperator(operator string) (string, error) { return "", fmt.Errorf("invalid operator: %s is unknown", operator) } } + +func rangeOperators(operator string) ([]string, error) { + switch operator { + case InsideRange: + return []string{">=", "AND", "<="}, nil + case OutsideRange: + return []string{"<", "OR", ">"}, nil + default: + return nil, fmt.Errorf("invalid operator: %s is unknown", operator) + } +} diff --git a/kapacitor/tickscripts_test.go b/kapacitor/tickscripts_test.go index e72ded908d..95f1bef258 100644 --- a/kapacitor/tickscripts_test.go +++ b/kapacitor/tickscripts_test.go @@ -199,16 +199,15 @@ trigger } } -func TestThresholdRange(t *testing.T) { +func TestThresholdInsideRange(t *testing.T) { alert := chronograf.AlertRule{ Name: "name", Trigger: "threshold", Alerts: []string{"slack", "victorops", "email"}, TriggerValues: chronograf.TriggerValues{ - Operator: "greater than", - Value: "90", - RangeOperator: "less than", - RangeValue: "100", + Operator: "is inside range", + Value: "90", + RangeValue: "100", }, Every: "30s", Message: "message", @@ -305,7 +304,154 @@ var data = stream var trigger = data |alert() - .crit(lambda: "value" > lower AND "value" < upper) + .crit(lambda: "value" >= lower AND "value" <= upper) + .stateChangesOnly() + .message(message) + .id(idVar) + .idTag(idTag) + .levelTag(levelTag) + .messageField(messageField) + .durationField(durationField) + .slack() + .victorOps() + .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 TestThresholdOutsideRange(t *testing.T) { + alert := chronograf.AlertRule{ + Name: "name", + Trigger: "threshold", + Alerts: []string{"slack", "victorops", "email"}, + TriggerValues: chronograf.TriggerValues{ + Operator: "is outside range", + Value: "90", + RangeValue: "100", + }, + Every: "30s", + Message: "message", + Query: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + { + Field: "usage_user", + Funcs: []string{"mean"}, + }, + }, + Tags: map[string][]string{ + "host": []string{ + "acc-0eabc309-eu-west-1-data-3", + "prod", + }, + "cpu": []string{ + "cpu_total", + }, + }, + GroupBy: chronograf.GroupBy{ + Time: "10m", + Tags: []string{"host", "cluster_id"}, + }, + AreTagsAccepted: true, + RawText: "", + }, + } + + tests := []struct { + name string + alert chronograf.AlertRule + want chronograf.TICKScript + wantErr bool + }{ + { + name: "Test valid template alert", + alert: alert, + want: `var db = 'telegraf' + +var rp = 'autogen' + +var measurement = 'cpu' + +var groupBy = ['host', 'cluster_id'] + +var whereFilter = lambda: ("cpu" == 'cpu_total') AND ("host" == 'acc-0eabc309-eu-west-1-data-3' OR "host" == 'prod') + +var period = 10m + +var every = 30s + +var name = 'name' + +var idVar = name + ':{{.Group}}' + +var message = 'message' + +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 lower = 90 + +var upper = 100 + +var data = stream + |from() + .database(db) + .retentionPolicy(rp) + .measurement(measurement) + .groupBy(groupBy) + .where(whereFilter) + |window() + .period(period) + .every(every) + .align() + |mean('usage_user') + .as('value') + +var trigger = data + |alert() + .crit(lambda: "value" < lower OR "value" > upper) .stateChangesOnly() .message(message) .id(idVar) diff --git a/kapacitor/triggers.go b/kapacitor/triggers.go index cc59587360..6efab3cbca 100644 --- a/kapacitor/triggers.go +++ b/kapacitor/triggers.go @@ -37,7 +37,7 @@ var ThresholdTrigger = ` var ThresholdRangeTrigger = ` var trigger = data |alert() - .crit(lambda: "value" %s lower AND "value" %s upper) + .crit(lambda: "value" %s lower %s "value" %s upper) ` // RelativeAbsoluteTrigger compares one window of data versus another (current - past) @@ -89,7 +89,7 @@ func Trigger(rule chronograf.AlertRule) (string, error) { case Relative: trigger, err = relativeTrigger(rule) case Threshold: - if rule.TriggerValues.RangeOperator == "" || rule.TriggerValues.RangeValue == "" { + if rule.TriggerValues.RangeValue == "" { trigger, err = thresholdTrigger(rule) } else { trigger, err = thresholdRangeTrigger(rule) @@ -128,13 +128,13 @@ func thresholdTrigger(rule chronograf.AlertRule) (string, error) { } func thresholdRangeTrigger(rule chronograf.AlertRule) (string, error) { - op, err := kapaOperator(rule.TriggerValues.Operator) + ops, err := rangeOperators(rule.TriggerValues.Operator) if err != nil { return "", err } - rangeOp, err := kapaOperator(rule.TriggerValues.RangeOperator) - if err != nil { - return "", err + var iops []interface{} = make([]interface{}, len(ops)) + for i, o := range ops { + iops[i] = o } - return fmt.Sprintf(ThresholdRangeTrigger, op, rangeOp), nil + return fmt.Sprintf(ThresholdRangeTrigger, iops...), nil } diff --git a/kapacitor/vars.go b/kapacitor/vars.go index 6387886507..4692fbd82d 100644 --- a/kapacitor/vars.go +++ b/kapacitor/vars.go @@ -34,7 +34,7 @@ func Vars(rule chronograf.AlertRule) (string, error) { switch rule.Trigger { case Threshold: - if rule.TriggerValues.RangeOperator == "" || rule.TriggerValues.RangeValue == "" { + if rule.TriggerValues.RangeValue == "" { vars := ` %s var crit = %s diff --git a/server/swagger.json b/server/swagger.json index 042bf44538..667e75cad4 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -1747,7 +1747,7 @@ } } } - }, + } }, "definitions": { "Kapacitors": { @@ -1837,9 +1837,84 @@ } } }, + "QueryConfig": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "database": { + "type": "string" + }, + "measurement": { + "type": "string" + }, + "retentionPolicy": { + "type": "string" + }, + "areTagsAccepted": { + "type": "boolean" + }, + "rawText": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "groupBy": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "time", + "tags" + ] + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "funcs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "field", + "funcs" + ] + } + } + }, + "required": [ + "database", + "measurement", + "retentionPolicy", + "areTagsAccepted", + "tags", + "groupBy", + "fields" + ] + }, "Rule": { "type": "object", "required": [ + "query", "every", "trigger" ], @@ -1848,6 +1923,10 @@ "type": "string", "description": "ID for this rule; the ID is shared with kapacitor" }, + "query": { + "description": "Query config structure is historical from chronograf 1.0", + "$ref": "#/definitions/QueryConfig" + }, "name": { "type": "string", "description": "User facing name of the alerting rule" @@ -1888,6 +1967,50 @@ "threshold" ] }, + "values": { + "type": "object", + "description": "Alerting logic for trigger type", + "properties": { + "change": { + "description": "Specifies if the change is percent or absolute", + "type": "string", + "enum": [ + "% change", + "change" + ] + }, + "period": { + "description": "Length of time before deadman is alerted (golang duration)", + "type": "string" + }, + "shift": { + "description": "Amount of time to look into the past to compare to the present (golang duration)", + "type": "string" + }, + "operator": { + "description": "Operator for alert comparison", + "type": "string", + "enum": [ + "greater than", + "less than", + "equal to or less than", + "equal to or greater", + "equal to", + "not equal to", + "is inside range", + "is outside range" + ] + }, + "value": { + "description": "Value is the boundary value when alert goes critical", + "type": "string" + }, + "rangeValue": { + "description": "Optional value for range comparisions", + "type": "string" + } + } + }, "tickscript": { "type": "string", "description": "TICKscript representing this rule" @@ -2438,4 +2561,4 @@ } } } -} +} \ No newline at end of file