diff --git a/config/config.go b/config/config.go index 8007657f..4e52ba87 100644 --- a/config/config.go +++ b/config/config.go @@ -389,6 +389,7 @@ type Receiver struct { SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty"` WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty"` OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty"` + PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` diff --git a/config/notifiers.go b/config/notifiers.go index 8af98da0..1dd5c206 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -16,6 +16,7 @@ package config import ( "fmt" "strings" + "time" ) var ( @@ -89,6 +90,19 @@ var ( Source: `{{ template "opsgenie.default.source" . }}`, // TODO: Add a details field with all the alerts. } + + // DefaultPushoverConfig defines default values for Pushover configurations. + DefaultPushoverConfig = PushoverConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + Title: `{{ template "pushover.default.title" . }}`, + Message: `{{ template "pushover.default.message" . }}`, + URL: `{{ template "pushover.default.url" . }}`, + Priority: `{{ if eq .Status "firing" }}2{{ else }}0{{ end }}`, // emergency (firing) or normal + Retry: duration(1 * time.Minute), + Expire: duration(1 * time.Hour), + } ) // NotifierConfig contains base options common across all notifier configurations. @@ -283,3 +297,49 @@ func (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error } return checkOverflow(c.XXX, "opsgenie config") } + +type duration time.Duration + +func (d *duration) UnmarshalText(text []byte) error { + parsed, err := time.ParseDuration(string(text)) + if err == nil { + *d = duration(parsed) + } + return err +} + +func (d duration) MarshalText() ([]byte, error) { + return []byte(time.Duration(d).String()), nil +} + +type PushoverConfig struct { + NotifierConfig `yaml:",inline"` + + UserKey Secret `yaml:"user_key"` + Token Secret `yaml:"token"` + Title string `yaml:"title"` + Message string `yaml:"message"` + URL string `yaml:"url"` + Priority string `yaml:"priority"` + Retry duration `yaml:"retry"` + Expire duration `yaml:"expire"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *PushoverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultPushoverConfig + type plain PushoverConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + if c.UserKey == "" { + return fmt.Errorf("missing user key in Pushover config") + } + if c.Token == "" { + return fmt.Errorf("missing token in Pushover config") + } + return checkOverflow(c.XXX, "pushover config") +} diff --git a/notify/impl.go b/notify/impl.go index a10054c3..a53045f4 100644 --- a/notify/impl.go +++ b/notify/impl.go @@ -19,11 +19,13 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "mime" "net" "net/http" "net/mail" "net/smtp" + "net/url" "os" "strings" "time" @@ -134,6 +136,10 @@ func Build(confs []*config.Receiver, tmpl *template.Template) map[string]Fanout n := NewHipchat(c, tmpl) add(i, n, filter(n, c)) } + for i, c := range nc.PushoverConfigs { + n := NewPushover(c, tmpl) + add(i, n, filter(n, c)) + } res[nc.Name] = fo } @@ -682,6 +688,76 @@ func (n *OpsGenie) Notify(ctx context.Context, as ...*types.Alert) error { return nil } +// Pushover implements a Notifier for Pushover notifications. +type Pushover struct { + conf *config.PushoverConfig + tmpl *template.Template +} + +// NewPushover returns a new Pushover notifier. +func NewPushover(c *config.PushoverConfig, t *template.Template) *Pushover { + return &Pushover{conf: c, tmpl: t} +} + +func (*Pushover) name() string { return "pushover" } + +// Notify implements the Notifier interface. +func (n *Pushover) Notify(ctx context.Context, as ...*types.Alert) error { + key, ok := GroupKey(ctx) + if !ok { + return fmt.Errorf("group key missing") + } + data := n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...) + + log.With("incident", key).Debugln("notifying Pushover") + + var err error + tmpl := tmplText(n.tmpl, data, &err) + + parameters := url.Values{} + parameters.Add("token", tmpl(string(n.conf.Token))) + parameters.Add("user", tmpl(string(n.conf.UserKey))) + title := tmpl(n.conf.Title) + message := tmpl(n.conf.Message) + parameters.Add("title", title) + if len(title)+len(message) > 512 { + message = message[:512] + log.With("incident", key).Debugf("Truncated message to %q due to Pushover message limit", message) + } + if message == "" { + // Pushover rejects empty messages. + message = "(no details)" + } + parameters.Add("message", message) + parameters.Add("url", tmpl(n.conf.URL)) + parameters.Add("priority", tmpl(n.conf.Priority)) + parameters.Add("retry", fmt.Sprintf("%d", int64(time.Duration(n.conf.Retry).Seconds()))) + parameters.Add("expire", fmt.Sprintf("%d", int64(time.Duration(n.conf.Expire).Seconds()))) + + apiURL := "https://api.pushover.net/1/messages.json" + u, err := url.Parse(apiURL) + if err != nil { + return err + } + u.RawQuery = parameters.Encode() + log.With("incident", key).Debugf("Pushover URL = %q", u.String()) + + resp, err := ctxhttp.Post(ctx, http.DefaultClient, u.String(), "text/plain", nil) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode/100 != 2 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return fmt.Errorf("unexpected status code %v (body: %s)", resp.StatusCode, string(body)) + } + return nil +} + func tmplText(tmpl *template.Template, data *template.Data, err *error) func(string) string { return func(name string) (s string) { if *err != nil { diff --git a/template/default.tmpl b/template/default.tmpl index 217b8831..eb54531c 100644 --- a/template/default.tmpl +++ b/template/default.tmpl @@ -162,3 +162,8 @@ SOFTWARE. {{ end }} + +{{ define "pushover.default.title" }}{{ template "__subject" . }}{{ end }} +{{ define "pushover.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }} +{{ template "__text_alert_list" .Alerts.Firing }}{{ end }} +{{ define "pushover.default.url" }}{{ template "__alertmanagerURL" . }}{{ end }}