Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prometheus's alertmanager notifier #269

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,35 @@ prefix: `consul-alerts/config/notifiers/ilert/`
| api-key | The API key of the alert source. (mandatory) |
| incident-key-template | Format of the incident key. [Default: `{{.Node}}:{{.Service}}:{{.Check}}` |

#### Prometheus

To enable the Prometheus built-in notifier, set
`consul-alerts/config/notifiers/prometheus/enabled` to `true`. Prometheus details
needs to be configured.

prefix: `consul-alerts/config/notifiers/prometheus/`

| key | description |
|--------------|-----------------------------------------------------------------------|
| enabled | Enable the prometheus notifier. [Default: false] |
| cluster-name | The name of the cluster. [Default: "Consul Alerts"] |
| base-urls | Base URLs of the Prometheus cluster (mandatory) |
| endpoint | The endpoint to append to the end of each base-url |
| payload | The payload to send to the Prometheus server (mandatory) |

The value of 'payload' must be a json map of type string. Value will be rendered as a template.
```
{
"alertName": "{{ .Check }}/{{ .Service }}/{{ .Node }}",
"host": "{{ .Node }}",
"service": "{{ .Service }}",
"severity": "{{ .Status }}",
"output": "{{ .Output }}",
"notes": "{{ .Notes }}",
"time": "{{ .Timestamp }}"
}
```

Health Check via API
--------------------

Expand Down
6 changes: 5 additions & 1 deletion consul-alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ func builtinNotifiers() map[string]notifier.Notifier {
awssnsNotifier := consulClient.AwsSnsNotifier()
victoropsNotifier := consulClient.VictorOpsNotifier()
httpEndpointNotifier := consulClient.HttpEndpointNotifier()
prometheusNotifier := consulClient.PrometheusNotifier()
ilertNotifier := consulClient.ILertNotifier()

notifiers := map[string]notifier.Notifier{}
Expand Down Expand Up @@ -288,7 +289,10 @@ func builtinNotifiers() map[string]notifier.Notifier {
}
if httpEndpointNotifier.Enabled {
notifiers[httpEndpointNotifier.NotifierName()] = httpEndpointNotifier
}
}
if prometheusNotifier.Enabled {
notifiers[prometheusNotifier.NotifierName()] = prometheusNotifier
}
if ilertNotifier.Enabled {
notifiers[ilertNotifier.NotifierName()] = ilertNotifier
}
Expand Down
16 changes: 16 additions & 0 deletions consul/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,18 @@ func (c *ConsulAlertClient) LoadConfig() {
case "consul-alerts/config/notifiers/http-endpoint/payload":
valErr = loadCustomValue(&config.Notifiers.HttpEndpoint.Payload, val, ConfigTypeStrMap)

// Prometheus notifier config
case "consul-alerts/config/notifiers/prometheus/enabled":
valErr = loadCustomValue(&config.Notifiers.Prometheus.Enabled, val, ConfigTypeBool)
case "consul-alerts/config/notifiers/prometheus/cluster-name":
valErr = loadCustomValue(&config.Notifiers.Prometheus.ClusterName, val, ConfigTypeString)
case "consul-alerts/config/notifiers/prometheus/base-urls":
valErr = loadCustomValue(&config.Notifiers.Prometheus.BaseURLs, val, ConfigTypeStrArray)
case "consul-alerts/config/notifiers/prometheus/endpoint":
valErr = loadCustomValue(&config.Notifiers.Prometheus.Endpoint, val, ConfigTypeString)
case "consul-alerts/config/notifiers/prometheus/payload":
valErr = loadCustomValue(&config.Notifiers.Prometheus.Payload, val, ConfigTypeStrMap)

// iLert notfier config
case "consul-alerts/config/notifiers/ilert/enabled":
valErr = loadCustomValue(&config.Notifiers.ILert.Enabled, val, ConfigTypeBool)
Expand Down Expand Up @@ -566,6 +578,10 @@ func (c *ConsulAlertClient) HttpEndpointNotifier() *notifier.HttpEndpointNotifie
return c.config.Notifiers.HttpEndpoint
}

func (c *ConsulAlertClient) PrometheusNotifier() *notifier.PrometheusNotifier {
return c.config.Notifiers.Prometheus
}

func (c *ConsulAlertClient) ILertNotifier() *notifier.ILertNotifier {
return c.config.Notifiers.ILert
}
Expand Down
9 changes: 8 additions & 1 deletion consul/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type Consul interface {
AwsSnsNotifier() *notifier.AwsSnsNotifier
VictorOpsNotifier() *notifier.VictorOpsNotifier
HttpEndpointNotifier() *notifier.HttpEndpointNotifier
PrometheusNotifier() *notifier.PrometheusNotifier
ILertNotifier() *notifier.ILertNotifier

CheckChangeThreshold() int
Expand Down Expand Up @@ -173,7 +174,12 @@ func DefaultAlertConfig() *ConsulAlertConfig {
httpEndpoint := &notifier.HttpEndpointNotifier{
Enabled: false,
ClusterName: "Consul-Alerts",
}
}

prometheus := &notifier.PrometheusNotifier{
Enabled: false,
ClusterName: "Consul-Alerts",
}

ilert := &notifier.ILertNotifier{
Enabled: false,
Expand All @@ -193,6 +199,7 @@ func DefaultAlertConfig() *ConsulAlertConfig {
AwsSns: awsSns,
VictorOps: victorOps,
HttpEndpoint: httpEndpoint,
Prometheus: prometheus,
ILert: ilert,
Custom: []string{},
}
Expand Down
3 changes: 3 additions & 0 deletions notifier/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type Notifiers struct {
AwsSns *AwsSnsNotifier `json:"awssns"`
VictorOps *VictorOpsNotifier `json:"victorops"`
HttpEndpoint *HttpEndpointNotifier `json:"http-endpoint"`
Prometheus *PrometheusNotifier `json:"prometheus"`
ILert *ILertNotifier `json:"ilert"`
Custom []string `json:"custom"`
}
Expand Down Expand Up @@ -83,6 +84,8 @@ func (n Notifiers) GetNotifier(name string) (Notifier, bool) {
return n.VictorOps, true
case n.HttpEndpoint != nil && n.HttpEndpoint.NotifierName() == name:
return n.HttpEndpoint, true
case n.Prometheus != nil && n.Prometheus.NotifierName() == name:
return n.Prometheus, true
case n.ILert != nil && n.ILert.NotifierName() == name:
return n.ILert, true

Expand Down
134 changes: 134 additions & 0 deletions notifier/prometheus-notifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package notifier

import (
"fmt"
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"html/template"

log "github.com/AcalephStorage/consul-alerts/Godeps/_workspace/src/github.com/Sirupsen/logrus"
)

type PrometheusNotifier struct {
Enabled bool
ClusterName string `json:"cluster-name"`
BaseURLs []string `json:"base-urls"`
Endpoint string `json:"endpoint"`
Payload map[string]string `json:"payload"`
}

type TemplatePayloadData struct {
Node string
Service string
Check string
Status string
Output string
Notes string
Timestamp string
}

// NotifierName provides name for notifier selection
func (notifier *PrometheusNotifier) NotifierName() string {
return "prometheus"
}

func (notifier *PrometheusNotifier) Copy() Notifier {
n := *notifier
return &n
}

func renderPayload(t TemplatePayloadData, templateFile string, defaultTemplate string) (string, error) {
var tmpl *template.Template
var err error
if templateFile == "" {
tmpl, err = template.New("base").Parse(defaultTemplate)
} else {
tmpl, err = template.ParseFiles(templateFile)
}

if err != nil {
return "", err
}

var body bytes.Buffer
if err := tmpl.Execute(&body, t); err != nil {
return "", err
}

return body.String(), nil
}

//Notify sends messages to the endpoint notifier
func (notifier *PrometheusNotifier) Notify(messages Messages) bool {
var values []map[string]map[string]string

for _, m := range messages {
value := map[string]string{}
t := TemplatePayloadData{
Node: m.Node,
Service: m.Service,
Check: m.Check,
Status: m.Status,
Output: m.Output,
Notes: m.Notes,
Timestamp: m.Timestamp.Format("2006-01-02T15:04:05-0700"),
}

for payloadKey, payloadVal := range notifier.Payload {
data, err := renderPayload(t, "", payloadVal)
if err != nil {
log.Println("Error rendering template: ", err)
return false
}
value[payloadKey] = string(data)
}

values = append(values, map[string]map[string]string{"labels": value})
}

requestBody, err := json.Marshal(values)
if err != nil {
log.Println("Unable to encode POST data")
return false
}

c := make(chan bool)
defer close(c)
for _, bu := range notifier.BaseURLs {
endpoint := fmt.Sprintf("%s%s", bu, notifier.Endpoint)

// Channel senders. Logging the result where needed, and sending status back
go func() {
if res, err := http.Post(endpoint, "application/json", bytes.NewBuffer(requestBody)); err != nil {
log.Printf(fmt.Sprintf("Unable to send data to Prometheus server (%s): %s", endpoint, err))
c <- false
} else {
defer res.Body.Close()
statusCode := res.StatusCode

if statusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
log.Printf(fmt.Sprintf("Unable to notify Prometheus server (%s): %s", endpoint, string(body)))
c <- false
} else {
log.Printf(fmt.Sprintf("Notification sent to Prometheus server (%s).", endpoint))
c <- true
}
}
}()
}

// Channel receiver. Making sure to return the final result in bool
for i := 0; i < len(notifier.BaseURLs); i++ {
select {
case r := <- c:
if (! r) {
return false
}
}
}

return true
}