Merge pull request #70 from prometheus/webhook

Add a generic webhook notifier.
This commit is contained in:
Fabian Reinartz 2015-05-28 01:04:45 +02:00
commit b66f938840
10 changed files with 165 additions and 19 deletions

View File

@ -16,6 +16,7 @@ following aspects:
* aggregating alerts by labelset
* handling notification repeats
* sending alert notifications via external services (currently email,
generic web hook,
[PagerDuty](http://www.pagerduty.com/),
[HipChat](http://www.hipchat.com/),
[Slack](http://www.slack.com/),

View File

@ -58,7 +58,7 @@ message HipChatConfig {
// Configuration for notification via Slack.
message SlackConfig {
// Slack webhook url, (https://api.slack.com/incoming-webhooks).
// Slack webhook URL, (https://api.slack.com/incoming-webhooks).
optional string webhook_url = 1;
// Slack channel override, (like #other-channel or @username).
optional string channel = 2;
@ -70,6 +70,7 @@ message SlackConfig {
optional bool send_resolved = 5 [default = false];
}
// Configuration for notification via Flowdock.
message FlowdockConfig {
// Flowdock flow API token.
optional string api_token = 1;
@ -81,6 +82,14 @@ message FlowdockConfig {
optional bool send_resolved = 4 [default = false];
}
// Configuration for notification via generic webhook.
message WebhookConfig {
// URL to send POST request to.
optional string url = 1;
// Notify when resolved.
optional bool send_resolved = 2 [default = false];
}
// Notification configuration definition.
message NotificationConfig {
// Name of this NotificationConfig. Referenced from AggregationRule.
@ -97,6 +106,8 @@ message NotificationConfig {
repeated SlackConfig slack_config = 6;
// Zero or more Flowdock notification configurations.
repeated FlowdockConfig flowdock_config = 7;
// Zero or more generic web hook notification configurations.
repeated WebhookConfig webhook_config = 8;
}
// A regex-based label filter used in aggregations.

View File

@ -15,6 +15,7 @@ It has these top-level messages:
HipChatConfig
SlackConfig
FlowdockConfig
WebhookConfig
NotificationConfig
Filter
AggregationRule
@ -186,7 +187,7 @@ func (m *HipChatConfig) GetSendResolved() bool {
// Configuration for notification via Slack.
type SlackConfig struct {
// Slack webhook url, (https://api.slack.com/incoming-webhooks).
// Slack webhook URL, (https://api.slack.com/incoming-webhooks).
WebhookUrl *string `protobuf:"bytes,1,opt,name=webhook_url" json:"webhook_url,omitempty"`
// Slack channel override, (like #other-channel or @username).
Channel *string `protobuf:"bytes,2,opt,name=channel" json:"channel,omitempty"`
@ -242,6 +243,7 @@ func (m *SlackConfig) GetSendResolved() bool {
return Default_SlackConfig_SendResolved
}
// Configuration for notification via Flowdock.
type FlowdockConfig struct {
// Flowdock flow API token.
ApiToken *string `protobuf:"bytes,1,opt,name=api_token" json:"api_token,omitempty"`
@ -288,6 +290,35 @@ func (m *FlowdockConfig) GetSendResolved() bool {
return Default_FlowdockConfig_SendResolved
}
// Configuration for notification via generic webhook.
type WebhookConfig struct {
// URL to send POST request to.
Url *string `protobuf:"bytes,1,opt,name=url" json:"url,omitempty"`
// Notify when resolved.
SendResolved *bool `protobuf:"varint,2,opt,name=send_resolved,def=0" json:"send_resolved,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *WebhookConfig) Reset() { *m = WebhookConfig{} }
func (m *WebhookConfig) String() string { return proto.CompactTextString(m) }
func (*WebhookConfig) ProtoMessage() {}
const Default_WebhookConfig_SendResolved bool = false
func (m *WebhookConfig) GetUrl() string {
if m != nil && m.Url != nil {
return *m.Url
}
return ""
}
func (m *WebhookConfig) GetSendResolved() bool {
if m != nil && m.SendResolved != nil {
return *m.SendResolved
}
return Default_WebhookConfig_SendResolved
}
// Notification configuration definition.
type NotificationConfig struct {
// Name of this NotificationConfig. Referenced from AggregationRule.
@ -303,8 +334,10 @@ type NotificationConfig struct {
// Zero or more slack notification configurations.
SlackConfig []*SlackConfig `protobuf:"bytes,6,rep,name=slack_config" json:"slack_config,omitempty"`
// Zero or more Flowdock notification configurations.
FlowdockConfig []*FlowdockConfig `protobuf:"bytes,7,rep,name=flowdock_config" json:"flowdock_config,omitempty"`
XXX_unrecognized []byte `json:"-"`
FlowdockConfig []*FlowdockConfig `protobuf:"bytes,7,rep,name=flowdock_config" json:"flowdock_config,omitempty"`
// Zero or more generic web hook notification configurations.
WebhookConfig []*WebhookConfig `protobuf:"bytes,8,rep,name=webhook_config" json:"webhook_config,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *NotificationConfig) Reset() { *m = NotificationConfig{} }
@ -360,6 +393,13 @@ func (m *NotificationConfig) GetFlowdockConfig() []*FlowdockConfig {
return nil
}
func (m *NotificationConfig) GetWebhookConfig() []*WebhookConfig {
if m != nil {
return m.WebhookConfig
}
return nil
}
// A regex-based label filter used in aggregations.
type Filter struct {
// The regex matching the label name.

View File

@ -57,7 +57,7 @@ func main() {
}
saveSilencesTicker := time.NewTicker(10 * time.Second)
go func() {
for _ = range saveSilencesTicker.C {
for range saveSilencesTicker.C {
if err := silencer.SaveToFile(*silencesFile); err != nil {
log.Error("Error saving silences to file: ", err)
}

View File

@ -33,14 +33,14 @@ type Alerts []*Alert
// Alert models an action triggered by Prometheus.
type Alert struct {
// Short summary of alert.
Summary string
Summary string `json:"summary"`
// Long description of alert.
Description string
Description string `json:"description"`
// Label value pairs for purpose of aggregation, matching, and disposition
// dispatching. This must minimally include an "alertname" label.
Labels AlertLabelSet
Labels AlertLabelSet `json:"labels"`
// Extra key/value information which is not used for aggregation.
Payload AlertPayload
Payload AlertPayload `json:"payload"`
}
func (a *Alert) Name() string {

View File

@ -403,7 +403,7 @@ func (s *memoryAlertManager) runIteration() {
// Run the memoryAlertManager's main dispatcher loop.
func (s *memoryAlertManager) Run() {
iterationTicker := time.NewTicker(time.Second)
for _ = range iterationTicker.C {
for range iterationTicker.C {
s.checkSanity()
s.runIteration()
}

View File

@ -374,6 +374,40 @@ func newFlowdockMessage(op notificationOp, config *pb.FlowdockConfig, a *Alert)
return msg
}
type webhookMessage struct {
Version string `json:"version"`
Status string `json:"status"`
Alerts []Alert `json:"alert"`
}
func (n *notifier) sendWebhookNotification(op notificationOp, config *pb.WebhookConfig, a *Alert) error {
status := ""
switch op {
case notificationOpTrigger:
status = "firing"
case notificationOpResolve:
status = "resolved"
}
msg := &webhookMessage{
Version: "1",
Status: status,
Alerts: []Alert{*a},
}
jsonMessage, err := json.Marshal(msg)
if err != nil {
return err
}
httpResponse, err := postJSON(jsonMessage, config.GetUrl())
if err != nil {
return err
}
if err := processResponse(httpResponse, "Webhook", a); err != nil {
return err
}
return nil
}
func postJSON(jsonMessage []byte, url string) (*http.Response, error) {
timeout := time.Duration(5 * time.Second)
client := http.Client{
@ -582,6 +616,14 @@ func (n *notifier) handleNotification(a *Alert, op notificationOp, config *pb.No
log.Errorln("Error sending Flowdock notification:", err)
}
}
for _, whConfig := range config.WebhookConfig {
if op == notificationOpResolve && !whConfig.GetSendResolved() {
continue
}
if err := n.sendWebhookNotification(op, whConfig, a); err != nil {
log.Errorln("Error sending Webhook notification:", err)
}
}
}
func (n *notifier) Dispatch() {

View File

@ -16,10 +16,17 @@ package manager
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"reflect"
"testing"
"time"
pb "github.com/prometheus/alertmanager/config/generated"
)
func TestWriteEmailBody(t *testing.T) {
@ -169,3 +176,48 @@ func TestGetSMTPAuth(t *testing.T) {
t.Errorf("PLAIN auth with bad host-port: expected error but got %T, %v", auth, cfg)
}
}
func TestSendWebhookNotification(t *testing.T) {
var body []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
body, err = ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("error reading webhook notification: %s", err)
}
}))
defer ts.Close()
config := &pb.WebhookConfig{
Url: &ts.URL,
}
alert := &Alert{
Summary: "Testsummary",
Description: "Test alert description, something went wrong here.",
Labels: AlertLabelSet{
"alertname": "TestAlert",
},
Payload: AlertPayload{
"payload_label1": "payload_value1",
},
}
n := &notifier{}
err := n.sendWebhookNotification(notificationOpTrigger, config, alert)
if err != nil {
t.Errorf("error sending webhook notification: %s", err)
}
var msg webhookMessage
err = json.Unmarshal(body, &msg)
if err != nil {
t.Errorf("error unmarshalling webhook notification: %s", err)
}
expected := webhookMessage{
Version: "1",
Status: "firing",
Alerts: []Alert{*alert},
}
if !reflect.DeepEqual(msg, expected) {
t.Errorf("incorrect webhook notification: Expected: %s Actual: %s", expected, msg)
}
}

View File

@ -25,19 +25,19 @@ import (
)
type AlertManagerService struct {
Manager manager.AlertManager
Silencer *manager.Silencer
Manager manager.AlertManager
Silencer *manager.Silencer
PathPrefix string
}
func (s AlertManagerService) Handler() http.Handler {
r := httprouter.New()
r.POST(s.PathPrefix + "api/alerts", s.addAlerts)
r.GET(s.PathPrefix + "api/silences", s.silenceSummary)
r.POST(s.PathPrefix + "api/silences", s.addSilence)
r.GET(s.PathPrefix + "api/silences/:id", s.getSilence)
r.POST(s.PathPrefix + "api/silences/:id", s.updateSilence)
r.DELETE(s.PathPrefix + "api/silences/:id", s.deleteSilence)
r.POST(s.PathPrefix+"api/alerts", s.addAlerts)
r.GET(s.PathPrefix+"api/silences", s.silenceSummary)
r.POST(s.PathPrefix+"api/silences", s.addSilence)
r.GET(s.PathPrefix+"api/silences/:id", s.getSilence)
r.POST(s.PathPrefix+"api/silences/:id", s.updateSilence)
r.DELETE(s.PathPrefix+"api/silences/:id", s.deleteSilence)
return r
}

View File

@ -24,7 +24,7 @@ type SilenceStatus struct {
}
type SilencesHandler struct {
Silencer *manager.Silencer
Silencer *manager.Silencer
PathPrefix string
}