diff --git a/README.md b/README.md index 2b8dfb94..0c2b92b1 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,11 @@ Ex. `emailer_only` would be located at `consul-alerts/config/notif-profiles/emai ".{1,}" : { "type" : "string" } } }, + "NotifTypeList": { + "type": "object", + "title": "Hash of types for a given Notifier.", + "description": "A listing of Notifier names with a listing value indicating the types for that Notifier to use. (e.g. EmailNotifier can have receiver types like 'admins' that refer to a list of email addresses)" + }, "VarOverrides": { "type": "object", "title": "Hash of Notifier variables to override.", @@ -247,6 +252,9 @@ Ex. `emailer_only` would be located at `consul-alerts/config/notif-profiles/emai "NotifList": { "log":false, "email":true + }, + "NotifTypeList": { + "email": ["admins", "users"] } } ``` @@ -399,20 +407,56 @@ The email and smtp details needs to be configured: prefix: `consul-alerts/config/notifiers/email/` -| key | description | -|--------------|----------------------------------------------------------------------------------| -| enabled | Enable the email notifier. [Default: false] | -| cluster-name | The name of the cluster. [Default: "Consul Alerts"] | -| url | The SMTP server url | -| port | The SMTP server port | -| username | The SMTP username | -| password | The SMTP password | -| sender-alias | The sender alias. [Default: "Consul Alerts"] | -| sender-email | The sender email | -| receivers | The emails of the receivers. JSON array of string | -| template | Path to custom email template. [Default: internal template] | -| one-per-alert| Whether to send one email per alert [Default: false] | -| one-per-node | Whether to send one email per node [Default: false] (overriden by one-per-alert) | +| key | description | +|--------------|-------------------------------------------------------------------------------------------------------| +| enabled | Enable the email notifier. [Default: false] | +| cluster-name | The name of the cluster. [Default: "Consul Alerts"] | +| url | The SMTP server url | +| port | The SMTP server port | +| username | The SMTP username | +| password | The SMTP password | +| sender-alias | The sender alias. [Default: "Consul Alerts"] | +| sender-email | The sender email | +| receivers | Types of email receivers and associated email addresses. Expanded below since receivers is a folder. | +| template | Path to custom email template. [Default: internal template] | +| one-per-alert| Whether to send one email per alert [Default: false] | +| one-per-node | Whether to send one email per node [Default: false] (overriden by one-per-alert) | + +This email receivers configuration allows custom keys nested under: + +prefix: `consul-alerts/config/notifiers/email/receivers/` + +|key | description | +|------------|-------------------------------------------------------------------------------------------------------------------------------------| +| any-string | A list of email addresses that will be used when the corresponding key is used in the NotifTypeList of a NotifProfile. JSON array. | +| any-string | Another list of email addresses as above. Any number of such key/value pairs can be nested under receivers. See example below. | + +**Example email configuration:** + +**Key:** `consul-alerts/config/notifiers/emailer/` + +**Value:** +``` +{ + "enabled": true, + "url": "smtp.example.com", + "port": 587, + "username": "someuser", + "password": "password1", + "sender-email": "no-reply@example.com", + "receivers/" +} +``` + +**Example email receivers configuration (note the keys can be whatever you want, and you refer to the keys in NotifProfile under NotifTypeList):** +**Key:** `consul-alerts/config/notifiers/emailer/receivers/` +``` +{ + "admins": ["admin1@example.com", "admin2@example.com"], + "users": ["user1@ample.com", "user2@example.com"], + "any-collection-of-emails-you-want": ["a@example.com", "b@example.com", "c@example.com"] +} +``` The template can be any go html template. An `TemplateData` instance will be passed to the template. diff --git a/check-handler.go b/check-handler.go index bf4d78bb..42df4495 100644 --- a/check-handler.go +++ b/check-handler.go @@ -125,19 +125,20 @@ func (c *CheckProcessor) notify(alerts []consul.Check) { for i, alert := range alerts { profileInfo := consulClient.GetProfileInfo(alert.Node, alert.ServiceID, alert.CheckID, alert.Status) messages[i] = notifier.Message{ - Node: alert.Node, - ServiceId: alert.ServiceID, - Service: alert.ServiceName, - CheckId: alert.CheckID, - Check: alert.Name, - Status: alert.Status, - Output: alert.Output, - Notes: alert.Notes, - Interval: profileInfo.Interval, - RmdCheck: time.Now(), - NotifList: profileInfo.NotifList, - VarOverrides: profileInfo.VarOverrides, - Timestamp: time.Now(), + Node: alert.Node, + ServiceId: alert.ServiceID, + Service: alert.ServiceName, + CheckId: alert.CheckID, + Check: alert.Name, + Status: alert.Status, + Output: alert.Output, + Notes: alert.Notes, + Interval: profileInfo.Interval, + RmdCheck: time.Now(), + NotifList: profileInfo.NotifList, + //NotifTypeList: profileInfo.NotifTypeList, + VarOverrides: profileInfo.VarOverrides, + Timestamp: time.Now(), } if profileInfo.Interval > 0 { switch alert.Status { diff --git a/consul/client.go b/consul/client.go index c8ac8989..623ddbd8 100644 --- a/consul/client.go +++ b/consul/client.go @@ -100,8 +100,16 @@ func (c *ConsulAlertClient) LoadConfig() { valErr = loadCustomValue(&config.Notifiers.Email.Password, val, ConfigTypeString) case "consul-alerts/config/notifiers/email/port": valErr = loadCustomValue(&config.Notifiers.Email.Port, val, ConfigTypeInt) - case "consul-alerts/config/notifiers/email/receivers": - valErr = loadCustomValue(&config.Notifiers.Email.Receivers, val, ConfigTypeStrArray) + case "consul-alerts/config/notifiers/email/receivers/": + kvmTemp := c.KvMap("consul-alerts/config/notifiers/email/receivers") + // only want the key at the end, so split on slashes and take the last item + kvm := make(map[string][]string, len(kvmTemp)) + for k, v := range kvmTemp { + kSplit := strings.Split(k, "/") + kvm[kSplit[len(kSplit)-1]] = v + } + convertedVal, _ := json.Marshal(kvm) + valErr = loadCustomValue(&config.Notifiers.Email.Receivers, convertedVal, ConfigTypeStrMap) case "consul-alerts/config/notifiers/email/sender-alias": valErr = loadCustomValue(&config.Notifiers.Email.SenderAlias, val, ConfigTypeString) case "consul-alerts/config/notifiers/email/sender-email": @@ -294,7 +302,7 @@ func loadCustomValue(configVariable interface{}, data []byte, cType configType) arrConfig := configVariable.(*[]string) err = json.Unmarshal(data, arrConfig) case ConfigTypeStrMap: - mapConfig := configVariable.(*map[string]string) + mapConfig := configVariable.(*map[string][]string) err = json.Unmarshal(data, mapConfig) } return err @@ -475,6 +483,22 @@ func (c *ConsulAlertClient) CustomNotifiers() (customNotifs map[string]string) { return customNotifs } +// KvMap returns a map of KV pairs found directly inside the passed path +func (c *ConsulAlertClient) KvMap(kvPath string) (kvMap map[string][]string) { + if kvPairs, _, err := c.api.KV().List(kvPath, nil); err == nil { + kvMap = make(map[string][]string) + for _, kvPair := range kvPairs { + if strings.HasSuffix(kvPair.Key, "/") { + continue + } + itemList := []string{} + json.Unmarshal(kvPair.Value, &itemList) + kvMap[string(kvPair.Key)] = itemList + } + } + return kvMap +} + func (c *ConsulAlertClient) NewAlertsWithFilter(nodeName string, serviceName string, checkName string, statuses []string, ignoreBlacklist bool) []Check { allChecks, _, _ := c.api.KV().List("consul-alerts/checks", nil) alerts := make([]Check, 0) diff --git a/consul/interface.go b/consul/interface.go index 5d43cf9b..c19bd60a 100644 --- a/consul/interface.go +++ b/consul/interface.go @@ -56,9 +56,10 @@ type Status struct { // ProfileInfo is for reading in JSON from profile keys type ProfileInfo struct { - Interval int - NotifList map[string]bool - VarOverrides notifier.Notifiers + Interval int + NotifList map[string]bool + NotifTypeList map[string][]string + VarOverrides notifier.Notifiers } // Consul interface provides access to consul client @@ -119,7 +120,7 @@ func DefaultAlertConfig() *ConsulAlertConfig { ClusterName: "Consul-Alerts", Enabled: false, SenderAlias: "Consul Alerts", - Receivers: []string{}, + Receivers: map[string][]string{}, } log := ¬ifier.LogNotifier{ diff --git a/notifier/email-notifier.go b/notifier/email-notifier.go index ec63fdc7..08b63ef9 100644 --- a/notifier/email-notifier.go +++ b/notifier/email-notifier.go @@ -14,16 +14,16 @@ var sendMail = smtp.SendMail type EmailNotifier struct { ClusterName string `json:"cluster-name"` Enabled bool - Template string `json:"template"` - Url string `json:"url"` - Port int `json:"port"` - Username string `json:"username"` - Password string `json:"password"` - SenderAlias string `json:"sender-alias"` - SenderEmail string `json:"sender-email"` - Receivers []string `json:"receivers"` - OnePerAlert bool `json:"one-per-alert"` - OnePerNode bool `json:"one-per-node"` + Template string `json:"template"` + Url string `json:"url"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + SenderAlias string `json:"sender-alias"` + SenderEmail string `json:"sender-email"` + Receivers map[string][]string `json:"receivers"` + OnePerAlert bool `json:"one-per-alert"` + OnePerNode bool `json:"one-per-node"` } // NotifierName provides name for notifier selection @@ -39,69 +39,48 @@ func (emailNotifier *EmailNotifier) Copy() Notifier { //Notify sends messages to the endpoint notifier func (emailNotifier *EmailNotifier) Notify(alerts Messages) bool { - overAllStatus, pass, warn, fail := alerts.Summary() - nodeMap := mapByNodes(alerts) + // Get a unique list of all email NotifTypeList values found in Messages. + // These should correspond to the keys nested under notifiers/email/receivers/ + emailTypes := make(map[string]bool) + for _, alert := range alerts { + for _, emailType := range alert.NotifTypeList["email"] { + emailTypes[emailType] = true + } + } - var emailDataList []TemplateData + success := true - if emailNotifier.OnePerAlert { - log.Println("Going to send one email per alert") - emailDataList = []TemplateData{} - for _, check := range alerts { + // Filter on each email receiver type, and send emails + for emailType, _ := range emailTypes { + success = success && emailNotifier.notifyByType(alerts, emailType) + } - singleAlertChecks := make(Messages, 0) - singleAlertChecks = append(singleAlertChecks, check) - singleAlertMap := mapByNodes(singleAlertChecks) + return success +} - alertStatus, alertPassing, alertWarnings, alertFailures := singleAlertChecks.Summary() +func (emailNotifier *EmailNotifier) notifyByType(alerts Messages, emailType string) bool { - alertClusterName := emailNotifier.ClusterName + " " + check.Node + " - " + check.CheckId + success := true - e := TemplateData{ - ClusterName: alertClusterName, - SystemStatus: alertStatus, - FailCount: alertFailures, - WarnCount: alertWarnings, - PassCount: alertPassing, - Nodes: singleAlertMap, - } - emailDataList = append(emailDataList, e) - } + filteredAlerts := filterMessagesByType(alerts, emailType) + emailTo := emailNotifier.filterReceiversByType(emailType) + + //overAllStatus, pass, warn, fail := filteredAlerts.Summary() + nodeMap := mapByNodes(filteredAlerts) + + var emailDataList []EmailData + + if emailNotifier.OnePerAlert { + log.Println("Going to send one email per alert") + emailDataList = emailNotifier.buildEmailDataOnePerAlert(filteredAlerts) } else if emailNotifier.OnePerNode { log.Println("Going to send one email per node") - emailDataList = []TemplateData{} - for nodeName, checks := range nodeMap { - singleNodeMap := mapByNodes(checks) - nodeStatus, nodePassing, nodeWarnings, nodeFailures := checks.Summary() - - nodeClusterName := emailNotifier.ClusterName + " " + nodeName - - e := TemplateData{ - ClusterName: nodeClusterName, - SystemStatus: nodeStatus, - FailCount: nodeFailures, - WarnCount: nodeWarnings, - PassCount: nodePassing, - Nodes: singleNodeMap, - } - emailDataList = append(emailDataList, e) - } + emailDataList = emailNotifier.buildEmailDataOnePerNode(filteredAlerts, nodeMap) } else { log.Println("Going to send one email for many alerts") - e := TemplateData{ - ClusterName: emailNotifier.ClusterName, - SystemStatus: overAllStatus, - FailCount: fail, - WarnCount: warn, - PassCount: pass, - Nodes: nodeMap, - } - - emailDataList = []TemplateData{e} + emailDataList = emailNotifier.buildEmailDataOneForManyAlerts(filteredAlerts, nodeMap) } - success := true - for _, e := range emailDataList { var renderedTemplate string @@ -124,24 +103,99 @@ Content-Type: text/html; charset="UTF-8"; `, emailNotifier.SenderAlias, emailNotifier.SenderEmail, - strings.Join(emailNotifier.Receivers, ", "), + strings.Join(emailTo, ", "), e.ClusterName, e.SystemStatus, renderedTemplate) addr := fmt.Sprintf("%s:%d", emailNotifier.Url, emailNotifier.Port) auth := smtp.PlainAuth("", emailNotifier.Username, emailNotifier.Password, emailNotifier.Url) - if err := sendMail(addr, auth, emailNotifier.SenderEmail, emailNotifier.Receivers, []byte(msg)); err != nil { + if err := sendMail(addr, auth, emailNotifier.SenderEmail, emailTo, []byte(msg)); err != nil { log.Println("Unable to send notification:", err) continue } log.Println("Email notification sent.") success = success && true } - return success } +func filterMessagesByType(alerts Messages, emailType string) Messages { + filteredAlerts := make(Messages, 0) + for _, alert := range alerts { + for _, nt := range alert.NotifTypeList["email"] { + if nt == emailType { + filteredAlerts = append(filteredAlerts, alert) + break + } + } + } + return filteredAlerts +} + +func (emailNotifier *EmailNotifier) filterReceiversByType(emailType string) []string { + return emailNotifier.Receivers[emailType] +} + +func (emailNotifier *EmailNotifier) buildEmailDataOnePerAlert(filteredAlerts Messages) []EmailData { + emailDataList := []EmailData{} + for _, check := range filteredAlerts { + + singleAlertChecks := make(Messages, 0) + singleAlertChecks = append(singleAlertChecks, check) + singleAlertMap := mapByNodes(singleAlertChecks) + + alertStatus, alertPassing, alertWarnings, alertFailures := singleAlertChecks.Summary() + + alertClusterName := emailNotifier.ClusterName + " " + check.Node + " - " + check.CheckId + + e := EmailData{ + ClusterName: alertClusterName, + SystemStatus: alertStatus, + FailCount: alertFailures, + WarnCount: alertWarnings, + PassCount: alertPassing, + Nodes: singleAlertMap, + } + emailDataList = append(emailDataList, e) + } + return emailDataList +} + +func (emailNotifier *EmailNotifier) buildEmailDataOnePerNode(filteredAlerts Messages, nodeMap map[string]Messages) []EmailData { + emailDataList := []EmailData{} + for nodeName, checks := range nodeMap { + singleNodeMap := mapByNodes(checks) + nodeStatus, nodePassing, nodeWarnings, nodeFailures := checks.Summary() + + nodeClusterName := emailNotifier.ClusterName + " " + nodeName + + e := EmailData{ + ClusterName: nodeClusterName, + SystemStatus: nodeStatus, + FailCount: nodeFailures, + WarnCount: nodeWarnings, + PassCount: nodePassing, + Nodes: singleNodeMap, + } + emailDataList = append(emailDataList, e) + } + return emailDataList +} + +func (emailNotifier *EmailNotifier) buildEmailDataOneForManyAlerts(filteredAlerts Messages, nodeMap map[string]Messages) []EmailData { + overAllStatus, pass, warn, fail := filteredAlerts.Summary() + e := EmailData{ + ClusterName: emailNotifier.ClusterName, + SystemStatus: overAllStatus, + FailCount: fail, + WarnCount: warn, + PassCount: pass, + Nodes: nodeMap, + } + return []EmailData{e} +} + func mapByNodes(alerts Messages) map[string]Messages { nodeMap := make(map[string]Messages) for _, alert := range alerts { diff --git a/notifier/email-notifier_test.go b/notifier/email-notifier_test.go index 525e8130..a5b2b8a0 100644 --- a/notifier/email-notifier_test.go +++ b/notifier/email-notifier_test.go @@ -19,7 +19,12 @@ func TestNotify(t *testing.T) { expectedAddr := fmt.Sprintf("%s:%d", host, port) expectedFrom := "sender@example.com" - expectedTo := []string{"test1@example.com", "test2@example.com"} + expectedTo := make(map[string][]string) + admins := []string{"testadmin1@example.com", "testadmin2@example.com"} + users := []string{"testuser1@example.com", "testuser2@example.com"} + expectedTo["admins"] = admins + expectedTo["users"] = users + expectedMsg := `From: "Some Sender" To: test1@example.com, test2@example.com Subject: Some Cluster is HEALTHY diff --git a/notifier/notifier.go b/notifier/notifier.go index 7d236755..82895cd1 100644 --- a/notifier/notifier.go +++ b/notifier/notifier.go @@ -17,19 +17,20 @@ Fail: %d, Warn: %d, Pass: %d ` type Message struct { - Node string - ServiceId string - Service string - CheckId string - Check string - Status string - Output string - Notes string - Interval int - RmdCheck time.Time - NotifList map[string]bool - VarOverrides Notifiers - Timestamp time.Time + Node string + ServiceId string + Service string + CheckId string + Check string + Status string + Output string + Notes string + Interval int + RmdCheck time.Time + NotifList map[string]bool + NotifTypeList map[string][]string + VarOverrides Notifiers + Timestamp time.Time } type Messages []Message