diff --git a/config/config.go b/config/config.go index 37729e0c..4277ea32 100644 --- a/config/config.go +++ b/config/config.go @@ -163,6 +163,17 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { pdc.URL = c.Global.PagerdutyURL } } + for _, ogc := range rcv.OpsGenieConfigs { + if ogc.APIHost == "" { + if c.Global.OpsGenieAPIHost == "" { + return fmt.Errorf("no global OpsGenie URL set") + } + ogc.APIHost = c.Global.OpsGenieAPIHost + } + if !strings.HasSuffix(ogc.APIHost, "/") { + ogc.APIHost += "/" + } + } names[rcv.Name] = struct{}{} } return checkOverflow(c.XXX, "config") @@ -170,16 +181,18 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { // DefaultGlobalConfig provides global default values. var DefaultGlobalConfig = GlobalConfig{ - PagerdutyURL: "https://events.pagerduty.com/generic/2010-04-15/create_event.json", + PagerdutyURL: "https://events.pagerduty.com/generic/2010-04-15/create_event.json", + OpsGenieAPIHost: "https://api.opsgenie.com/", } // GlobalConfig defines configuration parameters that are valid globally // unless overwritten. type GlobalConfig struct { - SMTPFrom string `yaml:"smtp_from"` - SMTPSmarthost string `yaml:"smtp_smarthost"` - SlackURL string `yaml:"slack_url"` - PagerdutyURL string `yaml:"pagerduty_url"` + SMTPFrom string `yaml:"smtp_from"` + SMTPSmarthost string `yaml:"smtp_smarthost"` + SlackURL string `yaml:"slack_url"` + PagerdutyURL string `yaml:"pagerduty_url"` + OpsGenieAPIHost string `yaml:"opsgenie_api_host"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -309,6 +322,7 @@ type Receiver struct { PushoverConfigs []*PushoverConfig `yaml:"pushover_configs"` SlackConfigs []*SlackConfig `yaml:"slack_configs"` WebhookConfigs []*WebhookConfig `yaml:"webhook_configs"` + OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` diff --git a/config/notifies.go b/config/notifies.go index be2d727a..ee9ced1b 100644 --- a/config/notifies.go +++ b/config/notifies.go @@ -47,6 +47,12 @@ var ( Text: `{{template "slack.default.text" . }}`, Fallback: `{{template "slack.default.fallback" . }}`, } + + // DefaultOpsGenieConfig defines default values for OpsGenie configurations. + DefaultOpsGenieConfig = OpsGenieConfig{ + Description: `{{template "opsgenie.default.description" .}}`, + // TODO: Add a details field with all the alerts. + } ) // FlowdockConfig configures notifications via Flowdock. @@ -280,3 +286,27 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } return checkOverflow(c.XXX, "slack config") } + +// OpsGenieConfig configures notifications via OpsGenie. +type OpsGenieConfig struct { + APIKey string `yaml:"api_key"` + APIHost string `yaml:"api_host"` + Description string `yaml:"description"` + Details map[string]string `yaml:"details"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultOpsGenieConfig + type plain OpsGenieConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + if c.APIKey == "" { + return fmt.Errorf("missing service key in OpsGenie config") + } + return checkOverflow(c.XXX, "opsgenie config") +} diff --git a/notify/impl.go b/notify/impl.go index dd7fd405..5fa0087e 100644 --- a/notify/impl.go +++ b/notify/impl.go @@ -58,6 +58,9 @@ func Build(confs []*config.Receiver, tmpl *template.Template) map[string]Fanout for i, c := range nc.PagerdutyConfigs { add(i, NewPagerDuty(c, tmpl)) } + for i, c := range nc.OpsGenieConfigs { + add(i, NewOpsGenie(c, tmpl)) + } res[nc.Name] = fo } @@ -516,3 +519,93 @@ func (n *Slack) Notify(ctx context.Context, as ...*types.Alert) error { return nil } + +// OpsGenie implements a Notifier for OpsGenie notifications. +type OpsGenie struct { + conf *config.OpsGenieConfig + tmpl *template.Template +} + +// NewOpsGenieDuty returns a new OpsGenie notifier. +func NewOpsGenie(c *config.OpsGenieConfig, t *template.Template) *OpsGenie { + return &OpsGenie{conf: c, tmpl: t} +} + +type opsGenieMessage struct { + APIKey string `json:"apiKey"` + Alias model.Fingerprint `json:"alias"` +} + +type opsGenieCreateMessage struct { + *opsGenieMessage `json:,inline` + Message string `json:"message"` + Details map[string]string `json:"details"` +} + +type opsGenieCloseMessage struct { + *opsGenieMessage `json:,inline` +} + +// Notify implements the Notifier interface. +func (n *OpsGenie) Notify(ctx context.Context, as ...*types.Alert) error { + data := generateTemplateData(ctx, as...) + + key, ok := GroupKey(ctx) + if !ok { + return fmt.Errorf("group key missing") + } + + log.With("incident", key).Debugln("notifying OpsGenie") + + var err error + tmpl := func(name string) (s string) { + if err != nil { + return + } + s, err = n.tmpl.ExecuteTextString(name, data) + return s + } + details := make(map[string]string, len(n.conf.Details)) + for k, v := range n.conf.Details { + details[k] = tmpl(v) + } + + var ( + msg interface{} + apiURL string + ) + + apiMsg := opsGenieMessage{ + APIKey: n.conf.APIKey, + Alias: key, + } + alerts := types.Alerts(as...) + switch alerts.Status() { + case model.AlertResolved: + apiURL = n.conf.APIHost + "v1/json/alert/close" + msg = &opsGenieCloseMessage{&apiMsg} + default: + apiURL = n.conf.APIHost + "v1/json/alert" + msg = &opsGenieCreateMessage{ + opsGenieMessage: &apiMsg, + Message: tmpl(n.conf.Description), + Details: details, + } + } + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(msg); err != nil { + return err + } + + resp, err := ctxhttp.Post(ctx, http.DefaultClient, apiURL, contentTypeJSON, &buf) + if err != nil { + return err + } + resp.Body.Close() + + if resp.StatusCode/100 != 2 { + return fmt.Errorf("unexpected status code %v", resp.StatusCode) + } + return nil +} diff --git a/template/default.tmpl b/template/default.tmpl index cc280eb0..6762ef1c 100644 --- a/template/default.tmpl +++ b/template/default.tmpl @@ -17,6 +17,8 @@ {{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }} +{{ define "opsgenie.default.description" }}{{ template "__subject" . }}{{ end }} + {{ define "email.default.html" }} {{/*