From bd6f3e1a6b8054ebe6b07ed3067e68432396d6ef Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Thu, 9 Feb 2017 00:10:23 -0600 Subject: [PATCH] Add kapa alert arguments and properties --- chronograf.go | 26 +++++----- kapacitor/alerts.go | 86 ++++++++++++++++++++++++++------- kapacitor/alerts_test.go | 97 ++++++++++++++++++++++++++++++++++++++ kapacitor/validate.go | 2 +- kapacitor/validate_test.go | 4 +- 5 files changed, 183 insertions(+), 32 deletions(-) diff --git a/chronograf.go b/chronograf.go index 309ef998e..f5f9f732c 100644 --- a/chronograf.go +++ b/chronograf.go @@ -104,16 +104,16 @@ type SourcesStore interface { // AlertRule represents rules for building a tickscript alerting task type AlertRule struct { - ID string `json:"id,omitempty"` // ID is the unique ID of the alert - Query QueryConfig `json:"query"` // Query is the filter of data for the alert. - Every string `json:"every"` // Every how often to check for the alerting criteria - Alerts []string `json:"alerts"` // Alerts name all the services to notify (e.g. pagerduty) - AlertNodes []KapacitorNode `json:"alertNodes"` // AlertNodes define additional arguments to alerts - Message string `json:"message"` // Message included with alert - Details string `json:"details"` // Details is generally used for the Email alert. If empty will not be added. - Trigger string `json:"trigger"` // Trigger is a type that defines when to trigger the alert - TriggerValues TriggerValues `json:"values"` // Defines the values that cause the alert to trigger - Name string `json:"name"` // Name is the user-defined name for the alert + ID string `json:"id,omitempty"` // ID is the unique ID of the alert + Query QueryConfig `json:"query"` // Query is the filter of data for the alert. + Every string `json:"every"` // Every how often to check for the alerting criteria + Alerts []string `json:"alerts"` // Alerts name all the services to notify (e.g. pagerduty) + AlertNodes []KapacitorNode `json:"alertNodes,omitempty"` // AlertNodes define additional arguments to alerts + Message string `json:"message"` // Message included with alert + Details string `json:"details"` // Details is generally used for the Email alert. If empty will not be added. + Trigger string `json:"trigger"` // Trigger is a type that defines when to trigger the alert + TriggerValues TriggerValues `json:"values"` // Defines the values that cause the alert to trigger + Name string `json:"name"` // Name is the user-defined name for the alert } // AlertRulesStore stores rules for building tickscript alerting tasks @@ -176,9 +176,9 @@ type QueryConfig struct { // KapacitorNode adds arguments and properties to an alert type KapacitorNode struct { - Name string `json:"name"` - Args []string `json:"args"` - Properties []*KapacitorProperty `json:"properties"` + Name string `json:"name"` + Args []string `json:"args"` + Properties []KapacitorProperty `json:"properties"` // In the future we could add chaining methods here. } diff --git a/kapacitor/alerts.go b/kapacitor/alerts.go index 861ea598e..b61594ce9 100644 --- a/kapacitor/alerts.go +++ b/kapacitor/alerts.go @@ -2,12 +2,13 @@ package kapacitor import ( "fmt" + "strings" "github.com/influxdata/chronograf" ) -func kapaService(alert string) (string, error) { - switch alert { +func kapaHandler(handler string) (string, error) { + switch handler { case "hipchat": return "hipChat", nil case "opsgenie": @@ -18,25 +19,78 @@ func kapaService(alert string) (string, error) { return "victorOps", nil case "smtp": return "email", nil - case "sensu", "slack", "email", "talk", "telegram": - return alert, nil + case "http": + return "post", nil + case "sensu", "slack", "email", "talk", "telegram", "post", "tcp": + return handler, nil default: - return "", fmt.Errorf("Unsupport alert %s", alert) + return "", fmt.Errorf("Unsupported alert handler %s", handler) } } +func toKapaFunc(method string, args []string) (string, error) { + if len(args) == 0 { + return fmt.Sprintf(".%s()", method), nil + } + params := make([]string, len(args)) + copy(params, args) + // Kapacitor strings are quoted + for i, p := range params { + params[i] = fmt.Sprintf("'%s'", p) + } + return fmt.Sprintf(".%s(%s)", method, strings.Join(params, ",")), nil +} + +func addAlertNodes(rule chronograf.AlertRule) (string, error) { + alert := "" + // Using a map to try to combine older API in .Alerts with .AlertNodes + nodes := map[string]chronograf.KapacitorNode{} + for _, node := range rule.AlertNodes { + handler, err := kapaHandler(node.Name) + if err != nil { + return "", err + } + nodes[handler] = node + } + + for _, a := range rule.Alerts { + handler, err := kapaHandler(a) + if err != nil { + return "", err + } + // If the this handler is not in nodes, then there are + // there are no arguments or properties + if _, ok := nodes[handler]; !ok { + alert = alert + fmt.Sprintf(".%s()", handler) + } + } + + for handler, node := range nodes { + service, err := toKapaFunc(handler, node.Args) + if err != nil { + return "", nil + } + alert = alert + service + for _, prop := range node.Properties { + alertProperty, err := toKapaFunc(prop.Name, prop.Args) + if err != nil { + return "", nil + } + alert = alert + alertProperty + } + } + return alert, nil +} + // AlertServices generates alert chaining methods to be attached to an alert from all rule Services func AlertServices(rule chronograf.AlertRule) (string, error) { - alert := "" - for _, service := range rule.Alerts { - srv, err := kapaService(service) - if err != nil { - return "", err - } - if err := ValidateAlert(srv); err != nil { - return "", err - } - alert = alert + fmt.Sprintf(".%s()", srv) + node, err := addAlertNodes(rule) + if err != nil { + return "", err } - return alert, nil + + if err := ValidateAlert(node); err != nil { + return "", err + } + return node, nil } diff --git a/kapacitor/alerts_test.go b/kapacitor/alerts_test.go index 5dbb33b77..10a8214bc 100644 --- a/kapacitor/alerts_test.go +++ b/kapacitor/alerts_test.go @@ -49,6 +49,103 @@ func TestAlertServices(t *testing.T) { .slack() `, }, + { + name: "Test single valid service and property", + rule: chronograf.AlertRule{ + Alerts: []string{"slack"}, + AlertNodes: []chronograf.KapacitorNode{ + { + Name: "slack", + Properties: []chronograf.KapacitorProperty{ + { + Name: "channel", + Args: []string{"#general"}, + }, + }, + }, + }, + }, + want: `alert() + .slack() + .channel('#general') +`, + }, + { + name: "Test tcp", + rule: chronograf.AlertRule{ + AlertNodes: []chronograf.KapacitorNode{ + { + Name: "tcp", + Args: []string{"myaddress:22"}, + }, + }, + }, + want: `alert() + .tcp('myaddress:22') +`, + }, + { + name: "Test tcp no argument", + rule: chronograf.AlertRule{ + AlertNodes: []chronograf.KapacitorNode{ + { + Name: "tcp", + }, + }, + }, + wantErr: true, + }, + { + name: "Test tcp no argument with other services", + rule: chronograf.AlertRule{ + Alerts: []string{"slack", "tcp", "email"}, + AlertNodes: []chronograf.KapacitorNode{ + { + Name: "tcp", + }, + }, + }, + wantErr: true, + }, + { + name: "Test http as post", + rule: chronograf.AlertRule{ + AlertNodes: []chronograf.KapacitorNode{ + { + Name: "http", + Args: []string{"http://myaddress"}, + }, + }, + }, + want: `alert() + .post('http://myaddress') +`, + }, + { + name: "Test post", + rule: chronograf.AlertRule{ + AlertNodes: []chronograf.KapacitorNode{ + { + Name: "post", + Args: []string{"http://myaddress"}, + }, + }, + }, + want: `alert() + .post('http://myaddress') +`, + }, + { + name: "Test http no arguments", + rule: chronograf.AlertRule{ + AlertNodes: []chronograf.KapacitorNode{ + { + Name: "http", + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { got, err := AlertServices(tt.rule) diff --git a/kapacitor/validate.go b/kapacitor/validate.go index da7d0a590..ed8dca32a 100644 --- a/kapacitor/validate.go +++ b/kapacitor/validate.go @@ -18,7 +18,7 @@ func ValidateAlert(service string) error { // Simple tick script to check alert service. // If a pipeline cannot be created then we know this is an invalid // service. At least with this version of kapacitor! - script := fmt.Sprintf("stream|from()|alert().%s()", service) + script := fmt.Sprintf("stream|from()|alert()%s", service) return validateTick(chronograf.TICKScript(script)) } diff --git a/kapacitor/validate_test.go b/kapacitor/validate_test.go index 701fe6708..06576585a 100644 --- a/kapacitor/validate_test.go +++ b/kapacitor/validate_test.go @@ -11,12 +11,12 @@ func TestValidateAlert(t *testing.T) { }{ { name: "Test valid template alert", - service: "slack", + service: ".slack()", wantErr: false, }, { name: "Test invalid template alert", - service: "invalid", + service: ".invalid()", wantErr: true, }, }