diff --git a/Godeps b/Godeps index aaef709f9a..c280e39912 100644 --- a/Godeps +++ b/Godeps @@ -10,6 +10,7 @@ github.com/influxdata/kapacitor 0eb8c348b210dd3d32cb240a417f9e6ded1b691d github.com/influxdata/usage-client 6d3895376368aa52a3a81d2a16e90f0f52371967 github.com/jessevdk/go-flags 4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa github.com/satori/go.uuid b061729afc07e77a8aa4fad0a2fd840958f1942a +github.com/sergi/go-diff 1d28411638c1e67fe1930830df207bef72496ae9 github.com/tylerb/graceful 50a48b6e73fcc75b45e22c05b79629a67c79e938 golang.org/x/net 749a502dd1eaf3e5bfd4f8956748c502357c0bbe golang.org/x/oauth2 1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5 diff --git a/chronograf.go b/chronograf.go index 44baee78d0..134934be12 100644 --- a/chronograf.go +++ b/chronograf.go @@ -128,32 +128,36 @@ 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 is the window to search for alerting criteria - 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 - Percentile string `json:"percentile,omitempty"` // Percentile is defined only when Relation is not "Once" - Relation string `json:"relation,omitempty"` // Relation defines the logic about how often the threshold is met to be an alert. + 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 +} + +// Field represent influxql fields and functions from the UI +type Field struct { + Field string `json:"field"` + Funcs []string `json:"funcs"` +} + +// GroupBy represents influxql group by tags from the UI +type GroupBy struct { + Time string `json:"time"` + Tags []string `json:"tags"` } // QueryConfig represents UI query from the data explorer type QueryConfig struct { - ID string `json:"id,omitempty"` - Database string `json:"database"` - Measurement string `json:"measurement"` - RetentionPolicy string `json:"retentionPolicy"` - Fields []struct { - Field string `json:"field"` - Funcs []string `json:"funcs"` - } `json:"fields"` - Tags map[string][]string `json:"tags"` - GroupBy struct { - Time string `json:"time"` - Tags []string `json:"tags"` - } `json:"groupBy"` - AreTagsAccepted bool `json:"areTagsAccepted"` - RawText string `json:"rawText,omitempty"` + ID string `json:"id,omitempty"` + Database string `json:"database"` + Measurement string `json:"measurement"` + RetentionPolicy string `json:"retentionPolicy"` + Fields []Field `json:"fields"` + Tags map[string][]string `json:"tags"` + GroupBy GroupBy `json:"groupBy"` + AreTagsAccepted bool `json:"areTagsAccepted"` + RawText string `json:"rawText,omitempty"` } // Server represents a proxy connection to an HTTP server diff --git a/kapacitor/client.go b/kapacitor/client.go index 012a2571bf..9901cba5fc 100644 --- a/kapacitor/client.go +++ b/kapacitor/client.go @@ -3,6 +3,7 @@ package kapacitor import ( "context" "fmt" + "path" "github.com/influxdata/chronograf" client "github.com/influxdata/kapacitor/client/v1" @@ -26,6 +27,7 @@ const ( type Task struct { ID string // Kapacitor ID Href string // Kapacitor relative URI + HrefOutput string // Kapacitor relative URI to HTTPOutNode TICKScript chronograf.TICKScript // TICKScript is the running script } @@ -66,6 +68,7 @@ func (c *Client) Create(ctx context.Context, rule chronograf.AlertRule) (*Task, return &Task{ ID: kapaID, Href: task.Link.Href, + HrefOutput: path.Join(task.Link.Href, HTTPEndpoint), TICKScript: script, }, nil } diff --git a/kapacitor/http_out.go b/kapacitor/http_out.go new file mode 100644 index 0000000000..848e44a970 --- /dev/null +++ b/kapacitor/http_out.go @@ -0,0 +1,13 @@ +package kapacitor + +import ( + "fmt" + + "github.com/influxdata/chronograf" +) + +const HTTPEndpoint = "output" + +func HTTPOut(rule chronograf.AlertRule) (string, error) { + return fmt.Sprintf(`trigger|httpOut('%s')`, HTTPEndpoint), nil +} diff --git a/kapacitor/influxout_test.go b/kapacitor/influxout_test.go index f6a45d9843..80526ad1a7 100644 --- a/kapacitor/influxout_test.go +++ b/kapacitor/influxout_test.go @@ -29,10 +29,7 @@ func TestInfluxOut(t *testing.T) { Name: "name", Trigger: "deadman", Query: chronograf.QueryConfig{ - Fields: []struct { - Field string `json:"field"` - Funcs []string `json:"funcs"` - }{ + Fields: []chronograf.Field{ { Field: "usage_user", Funcs: []string{"mean"}, diff --git a/kapacitor/operators.go b/kapacitor/operators.go new file mode 100644 index 0000000000..59a34dadd6 --- /dev/null +++ b/kapacitor/operators.go @@ -0,0 +1,32 @@ +package kapacitor + +import "fmt" + +const ( + GreaterThan = "greater than" + LessThan = "less than" + LessThanEqual = "equal to or less than" + GreaterThanEqual = "equal to or greater than" + Equal = "equal" + NotEqual = "not equal" +) + +// kapaOperator converts UI strings to kapacitor operators +func kapaOperator(operator string) (string, error) { + switch operator { + case GreaterThan: + return ">", nil + case LessThan: + return "<", nil + case LessThanEqual: + return "<=", nil + case GreaterThanEqual: + return ">=", nil + case Equal: + return "==", nil + case NotEqual: + return "!=", nil + default: + return "", fmt.Errorf("invalid operator: %s is unknown", operator) + } +} diff --git a/kapacitor/templates.go b/kapacitor/templates.go deleted file mode 100644 index ff18178fe6..0000000000 --- a/kapacitor/templates.go +++ /dev/null @@ -1 +0,0 @@ -package kapacitor diff --git a/kapacitor/tickscripts.go b/kapacitor/tickscripts.go index ee8dff6425..4deefdf0ac 100644 --- a/kapacitor/tickscripts.go +++ b/kapacitor/tickscripts.go @@ -33,7 +33,12 @@ func (a *Alert) Generate(rule chronograf.AlertRule) (chronograf.TICKScript, erro if err != nil { return "", err } - raw := fmt.Sprintf("%s\n%s\n%s%s\n%s", vars, data, trigger, services, output) + http, err := HTTPOut(rule) + if err != nil { + return "", err + } + + raw := fmt.Sprintf("%s\n%s\n%s%s\n%s\n%s", vars, data, trigger, services, output, http) tick, err := formatTick(raw) if err != nil { return "", err diff --git a/kapacitor/tickscripts_test.go b/kapacitor/tickscripts_test.go index 8af90818f3..9043137a67 100644 --- a/kapacitor/tickscripts_test.go +++ b/kapacitor/tickscripts_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/influxdata/chronograf" + "github.com/sergi/go-diff/diffmatchpatch" ) func TestGenerate(t *testing.T) { @@ -14,7 +15,6 @@ func TestGenerate(t *testing.T) { Alerts: []string{"slack", "victorops", "email"}, TriggerValues: chronograf.TriggerValues{ Change: "change", - Period: "10m", Shift: "1m", Operator: "greater than", Value: "90", @@ -24,10 +24,7 @@ func TestGenerate(t *testing.T) { Database: "telegraf", Measurement: "cpu", RetentionPolicy: "autogen", - Fields: []struct { - Field string `json:"field"` - Funcs []string `json:"funcs"` - }{ + Fields: []chronograf.Field{ { Field: "usage_user", Funcs: []string{"mean"}, @@ -42,11 +39,8 @@ func TestGenerate(t *testing.T) { "cpu_total", }, }, - GroupBy: struct { - Time string `json:"time"` - Tags []string `json:"tags"` - }{ - Time: "", + GroupBy: chronograf.GroupBy{ + Time: "10m", Tags: []string{"host", "cluster_id"}, }, AreTagsAccepted: true, @@ -57,7 +51,7 @@ func TestGenerate(t *testing.T) { tick, err := gen.Generate(alert) if err != nil { fmt.Printf("%s", tick) - t.Errorf("Error generating alert: %v", err) + t.Errorf("Error generating alert: %v %s", err, tick) } } @@ -67,11 +61,8 @@ func TestThreshold(t *testing.T) { Trigger: "threshold", Alerts: []string{"slack", "victorops", "email"}, TriggerValues: chronograf.TriggerValues{ - Relation: "once", - Period: "10m", - Percentile: "", // TODO: if relation is not once then this will have a number - Operator: "greater than", - Value: "90", + Operator: "greater than", + Value: "90", }, Every: "30s", Message: "message", @@ -79,10 +70,7 @@ func TestThreshold(t *testing.T) { Database: "telegraf", Measurement: "cpu", RetentionPolicy: "autogen", - Fields: []struct { - Field string `json:"field"` - Funcs []string `json:"funcs"` - }{ + Fields: []chronograf.Field{ { Field: "usage_user", Funcs: []string{"mean"}, @@ -97,11 +85,8 @@ func TestThreshold(t *testing.T) { "cpu_total", }, }, - GroupBy: struct { - Time string `json:"time"` - Tags []string `json:"tags"` - }{ - Time: "", + GroupBy: chronograf.GroupBy{ + Time: "10m", Tags: []string{"host", "cluster_id"}, }, AreTagsAccepted: true, @@ -130,6 +115,8 @@ var whereFilter = lambda: ("cpu" == 'cpu_total') AND ("host" == 'acc-0eabc309-eu var period = 10m +var every = 30s + var name = 'name' var idVar = name + ':{{.Group}}' @@ -152,8 +139,6 @@ var outputMeasurement = 'alerts' var triggerType = 'threshold' -var every = 30s - var crit = 90 var data = stream @@ -172,8 +157,8 @@ var data = stream var trigger = data |alert() - .stateChangesOnly() .crit(lambda: "value" > crit) + .stateChangesOnly() .message(message) .id(idVar) .idTag(idTag) @@ -192,6 +177,9 @@ trigger .measurement(outputMeasurement) .tag('alertName', name) .tag('triggerType', triggerType) + +trigger + |httpOut('output') `, wantErr: false, }, @@ -204,8 +192,9 @@ trigger continue } if got != tt.want { - fmt.Printf("%s", got) - t.Errorf("%q. Threshold() = %v, want %v", tt.name, 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)) } } } @@ -216,11 +205,8 @@ func TestThresholdNoAggregate(t *testing.T) { Trigger: "threshold", Alerts: []string{"slack", "victorops", "email"}, TriggerValues: chronograf.TriggerValues{ - Relation: "once", - Period: "10m", - Percentile: "", // TODO: if relation is not once then this will have a number - Operator: "greater than", - Value: "90", + Operator: "greater than", + Value: "90", }, Every: "30s", Message: "message", @@ -228,10 +214,7 @@ func TestThresholdNoAggregate(t *testing.T) { Database: "telegraf", Measurement: "cpu", RetentionPolicy: "autogen", - Fields: []struct { - Field string `json:"field"` - Funcs []string `json:"funcs"` - }{ + Fields: []chronograf.Field{ { Field: "usage_user", Funcs: []string{}, @@ -246,11 +229,8 @@ func TestThresholdNoAggregate(t *testing.T) { "cpu_total", }, }, - GroupBy: struct { - Time string `json:"time"` - Tags []string `json:"tags"` - }{ - Time: "", + GroupBy: chronograf.GroupBy{ + Time: "10m", Tags: []string{"host", "cluster_id"}, }, AreTagsAccepted: true, @@ -277,8 +257,6 @@ 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 name = 'name' var idVar = name + ':{{.Group}}' @@ -301,8 +279,6 @@ var outputMeasurement = 'alerts' var triggerType = 'threshold' -var every = 30s - var crit = 90 var data = stream @@ -317,8 +293,8 @@ var data = stream var trigger = data |alert() - .stateChangesOnly() .crit(lambda: "value" > crit) + .stateChangesOnly() .message(message) .id(idVar) .idTag(idTag) @@ -337,6 +313,9 @@ trigger .measurement(outputMeasurement) .tag('alertName', name) .tag('triggerType', triggerType) + +trigger + |httpOut('output') `, wantErr: false, }, @@ -349,8 +328,9 @@ trigger continue } if got != tt.want { - fmt.Printf("%s", got) - t.Errorf("%q. Threshold() = %v, want %v", tt.name, 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)) } } } @@ -362,7 +342,6 @@ func TestRelative(t *testing.T) { Alerts: []string{"slack", "victorops", "email"}, TriggerValues: chronograf.TriggerValues{ Change: "% change", - Period: "10m", Shift: "1m", Operator: "greater than", Value: "90", @@ -373,10 +352,7 @@ func TestRelative(t *testing.T) { Database: "telegraf", Measurement: "cpu", RetentionPolicy: "autogen", - Fields: []struct { - Field string `json:"field"` - Funcs []string `json:"funcs"` - }{ + Fields: []chronograf.Field{ { Field: "usage_user", Funcs: []string{"mean"}, @@ -391,11 +367,8 @@ func TestRelative(t *testing.T) { "cpu_total", }, }, - GroupBy: struct { - Time string `json:"time"` - Tags []string `json:"tags"` - }{ - Time: "", + GroupBy: chronograf.GroupBy{ + Time: "10m", Tags: []string{"host", "cluster_id"}, }, AreTagsAccepted: true, @@ -424,6 +397,8 @@ var whereFilter = lambda: ("cpu" == 'cpu_total') AND ("host" == 'acc-0eabc309-eu var period = 10m +var every = 30s + var name = 'name' var idVar = name + ':{{.Group}}' @@ -446,8 +421,6 @@ var outputMeasurement = 'alerts' var triggerType = 'relative' -var every = 30s - var shift = -1m var crit = 90 @@ -478,8 +451,8 @@ var trigger = past .keep() .as('value') |alert() - .stateChangesOnly() .crit(lambda: "value" > crit) + .stateChangesOnly() .message(message) .id(idVar) .idTag(idTag) @@ -498,6 +471,9 @@ trigger .measurement(outputMeasurement) .tag('alertName', name) .tag('triggerType', triggerType) + +trigger + |httpOut('output') `, wantErr: false, }, @@ -510,8 +486,9 @@ trigger continue } if got != tt.want { - fmt.Printf("%s", got) - t.Errorf("%q. Relative() = %v, want %v", tt.name, 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)) } } } @@ -523,7 +500,6 @@ func TestRelativeChange(t *testing.T) { Alerts: []string{"slack", "victorops", "email"}, TriggerValues: chronograf.TriggerValues{ Change: "change", - Period: "10m", Shift: "1m", Operator: "greater than", Value: "90", @@ -534,10 +510,7 @@ func TestRelativeChange(t *testing.T) { Database: "telegraf", Measurement: "cpu", RetentionPolicy: "autogen", - Fields: []struct { - Field string `json:"field"` - Funcs []string `json:"funcs"` - }{ + Fields: []chronograf.Field{ { Field: "usage_user", Funcs: []string{"mean"}, @@ -552,11 +525,8 @@ func TestRelativeChange(t *testing.T) { "cpu_total", }, }, - GroupBy: struct { - Time string `json:"time"` - Tags []string `json:"tags"` - }{ - Time: "", + GroupBy: chronograf.GroupBy{ + Time: "10m", Tags: []string{"host", "cluster_id"}, }, AreTagsAccepted: true, @@ -585,6 +555,8 @@ var whereFilter = lambda: ("cpu" == 'cpu_total') AND ("host" == 'acc-0eabc309-eu var period = 10m +var every = 30s + var name = 'name' var idVar = name + ':{{.Group}}' @@ -607,8 +579,6 @@ var outputMeasurement = 'alerts' var triggerType = 'relative' -var every = 30s - var shift = -1m var crit = 90 @@ -639,8 +609,8 @@ var trigger = past .keep() .as('value') |alert() - .stateChangesOnly() .crit(lambda: "value" > crit) + .stateChangesOnly() .message(message) .id(idVar) .idTag(idTag) @@ -659,6 +629,9 @@ trigger .measurement(outputMeasurement) .tag('alertName', name) .tag('triggerType', triggerType) + +trigger + |httpOut('output') `, wantErr: false, }, @@ -671,8 +644,9 @@ trigger continue } if got != tt.want { - fmt.Printf("%s", got) - t.Errorf("%q. Relative() = %v, want %v", tt.name, 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)) } } } @@ -691,10 +665,7 @@ func TestDeadman(t *testing.T) { Database: "telegraf", Measurement: "cpu", RetentionPolicy: "autogen", - Fields: []struct { - Field string `json:"field"` - Funcs []string `json:"funcs"` - }{ + Fields: []chronograf.Field{ { Field: "usage_user", Funcs: []string{"mean"}, @@ -709,10 +680,7 @@ func TestDeadman(t *testing.T) { "cpu_total", }, }, - GroupBy: struct { - Time string `json:"time"` - Tags []string `json:"tags"` - }{ + GroupBy: chronograf.GroupBy{ Time: "", Tags: []string{"host", "cluster_id"}, }, @@ -798,6 +766,9 @@ trigger .measurement(outputMeasurement) .tag('alertName', name) .tag('triggerType', triggerType) + +trigger + |httpOut('output') `, wantErr: false, }, @@ -810,8 +781,9 @@ trigger continue } if got != tt.want { - fmt.Printf("%s", got) - t.Errorf("%q. Deadman() = %v, want %v", tt.name, 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)) } } } diff --git a/kapacitor/triggers.go b/kapacitor/triggers.go index dd9a97f83a..be682e344d 100644 --- a/kapacitor/triggers.go +++ b/kapacitor/triggers.go @@ -3,12 +3,18 @@ package kapacitor import "github.com/influxdata/chronograf" import "fmt" -// ThresholdTrigger is the trickscript trigger for alerts that exceed a value -var ThresholdTrigger = ` - var trigger = data - |alert() +const ( + // Deadman triggers when data is missing for a period of time + Deadman = "deadman" + // Relative triggers when the value has changed compared to the past + Relative = "relative" + // Threshold triggers when value crosses a threshold + Threshold = "threshold" +) + +// AllAlerts are properties all alert types will have +var AllAlerts = ` .stateChangesOnly() - .crit(lambda: "value" %s crit) .message(message) .id(idVar) .idTag(idTag) @@ -17,6 +23,13 @@ var ThresholdTrigger = ` .durationField(durationField) ` +// ThresholdTrigger is the trickscript trigger for alerts that exceed a value +var ThresholdTrigger = ` + var trigger = data + |alert() + .crit(lambda: "value" %s crit) +` + // RelativeAbsoluteTrigger compares one window of data versus another (current - past) var RelativeAbsoluteTrigger = ` var past = data @@ -31,14 +44,7 @@ var trigger = past .keep() .as('value') |alert() - .stateChangesOnly() .crit(lambda: "value" %s crit) - .message(message) - .id(idVar) - .idTag(idTag) - .levelTag(levelTag) - .messageField(messageField) - .durationField(durationField) ` // RelativePercentTrigger compares one window of data versus another as a percent change. @@ -55,72 +61,54 @@ var trigger = past .keep() .as('value') |alert() - .stateChangesOnly() .crit(lambda: "value" %s crit) - .message(message) - .id(idVar) - .idTag(idTag) - .levelTag(levelTag) - .messageField(messageField) - .durationField(durationField) ` // DeadmanTrigger checks if any data has been streamed in the last period of time var DeadmanTrigger = ` var trigger = data|deadman(threshold, period) - .stateChangesOnly() - .message(message) - .id(idVar) - .idTag(idTag) - .levelTag(levelTag) - .messageField(messageField) - .durationField(durationField) ` // Trigger returns the trigger mechanism for a tickscript func Trigger(rule chronograf.AlertRule) (string, error) { + var trigger string + var err error switch rule.Trigger { - case "deadman": - return DeadmanTrigger, nil - case "relative": - op, err := kapaOperator(rule.TriggerValues.Operator) - if err != nil { - return "", err - } - if rule.TriggerValues.Change == "% change" { - return fmt.Sprintf(RelativePercentTrigger, op), nil - } else if rule.TriggerValues.Change == "change" { - return fmt.Sprintf(RelativeAbsoluteTrigger, op), nil - } else { - return "", fmt.Errorf("Unknown change type %s", rule.TriggerValues.Change) - } - case "threshold": - op, err := kapaOperator(rule.TriggerValues.Operator) - if err != nil { - return "", err - } - return fmt.Sprintf(ThresholdTrigger, op), nil + case Deadman: + trigger, err = DeadmanTrigger, nil + case Relative: + trigger, err = relativeTrigger(rule) + case Threshold: + trigger, err = thresholdTrigger(rule) default: - return "", fmt.Errorf("Unknown trigger type: %s", rule.Trigger) + trigger, err = "", fmt.Errorf("Unknown trigger type: %s", rule.Trigger) + } + + if err != nil { + return "", err + } + + return trigger + AllAlerts, nil +} + +func relativeTrigger(rule chronograf.AlertRule) (string, error) { + op, err := kapaOperator(rule.TriggerValues.Operator) + if err != nil { + return "", err + } + if rule.TriggerValues.Change == "% change" { + return fmt.Sprintf(RelativePercentTrigger, op), nil + } else if rule.TriggerValues.Change == "change" { + return fmt.Sprintf(RelativeAbsoluteTrigger, op), nil + } else { + return "", fmt.Errorf("Unknown change type %s", rule.TriggerValues.Change) } } -// kapaOperator converts UI strings to kapacitor operators -func kapaOperator(operator string) (string, error) { - switch operator { - case "greater than": - return ">", nil - case "less than": - return "<", nil - case "equal to or less than": - return "<=", nil - case "equal to or greater than": - return ">=", nil - case "equal": - return "==", nil - case "not equal": - return "!=", nil - default: - return "", fmt.Errorf("invalid operator: %s is unknown", operator) +func thresholdTrigger(rule chronograf.AlertRule) (string, error) { + op, err := kapaOperator(rule.TriggerValues.Operator) + if err != nil { + return "", err } + return fmt.Sprintf(ThresholdTrigger, op), nil } diff --git a/kapacitor/triggers_test.go b/kapacitor/triggers_test.go index 262c324831..4d8fce8892 100644 --- a/kapacitor/triggers_test.go +++ b/kapacitor/triggers_test.go @@ -51,8 +51,8 @@ var trigger = past .keep() .as('value') |alert() - .stateChangesOnly() .crit(lambda: "value" > crit) + .stateChangesOnly() .message(message) .id(idVar) .idTag(idTag) @@ -83,8 +83,8 @@ var trigger = past .keep() .as('value') |alert() - .stateChangesOnly() .crit(lambda: "value" > crit) + .stateChangesOnly() .message(message) .id(idVar) .idTag(idTag) @@ -104,8 +104,8 @@ var trigger = past }, want: `var trigger = data |alert() - .stateChangesOnly() .crit(lambda: "value" > crit) + .stateChangesOnly() .message(message) .id(idVar) .idTag(idTag) diff --git a/kapacitor/vars.go b/kapacitor/vars.go index 49bc2bf5b9..5f436aaad5 100644 --- a/kapacitor/vars.go +++ b/kapacitor/vars.go @@ -33,30 +33,26 @@ func Vars(rule chronograf.AlertRule) (string, error) { } switch rule.Trigger { - case "threshold": + case Threshold: vars := ` %s - var every = %s var crit = %s ` return fmt.Sprintf(vars, common, - rule.Every, rule.TriggerValues.Value), nil - case "relative": + case Relative: vars := ` %s - var every = %s var shift = -%s var crit = %s ` return fmt.Sprintf(vars, common, - rule.Every, rule.TriggerValues.Shift, rule.TriggerValues.Value, ), nil - case "deadman": + case Deadman: vars := ` %s var threshold = %s @@ -77,7 +73,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) { var measurement = '%s' var groupBy = %s var whereFilter = %s - var period = %s + %s var name = '%s' var idVar = name + ':{{.Group}}' @@ -98,7 +94,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) { rule.Query.Measurement, groupBy(rule.Query), whereFilter(rule.Query), - rule.TriggerValues.Period, + window(rule), rule.Name, rule.Message, IDTag, @@ -112,6 +108,21 @@ func commonVars(rule chronograf.AlertRule) (string, error) { ), nil } +// window is only used if deadman or threshold/relative with aggregate. Will return empty +// if no period. +func window(rule chronograf.AlertRule) string { + if rule.Trigger == Deadman { + return fmt.Sprintf("var period = %s", rule.TriggerValues.Period) + } + // Period only makes sense if the field has a been grouped via a time duration. + for _, field := range rule.Query.Fields { + if len(field.Funcs) > 0 { + return fmt.Sprintf("var period = %s\nvar every = %s", rule.Query.GroupBy.Time, rule.Every) + } + } + return "" +} + func groupBy(q chronograf.QueryConfig) string { groups := []string{} for _, tag := range q.GroupBy.Tags { diff --git a/server/kapacitors.go b/server/kapacitors.go index bb25e0b3a7..b8e60862e9 100644 --- a/server/kapacitors.go +++ b/server/kapacitors.go @@ -39,7 +39,7 @@ func (p *postKapacitorRequest) Valid() error { type kapaLinks struct { Proxy string `json:"proxy"` // URL location of proxy endpoint for this source Self string `json:"self"` // Self link mapping to this resource - Rules string `json:"rules"` // Riles link for defining roles alerts for kapacitor + Rules string `json:"rules"` // Rules link for defining roles alerts for kapacitor } type kapacitor struct { @@ -323,6 +323,7 @@ func (h *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) { Links: alertLinks{ Self: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/rules/%s", srv.SrcID, srv.ID, req.ID), Kapacitor: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.Href)), + Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.HrefOutput)), }, TICKScript: string(task.TICKScript), } @@ -334,6 +335,7 @@ func (h *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) { type alertLinks struct { Self string `json:"self"` Kapacitor string `json:"kapacitor"` + Output string `json:"output"` } type alertResponse struct {