diff --git a/config/config.go b/config/config.go index e2c08e4eca..5610b0b658 100644 --- a/config/config.go +++ b/config/config.go @@ -458,6 +458,7 @@ type Receiver struct { HipchatConfigs []*HipchatConfig `yaml:"hipchat_configs,omitempty" json:"hipchat_configs,omitempty"` SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` + EtcdConfigs []*EtcdConfig `yaml:"etcd_configs,omitempty" json:"etcd_configs,omitempty"` OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"` PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` diff --git a/config/notifiers.go b/config/notifiers.go index 68c411b5a6..72e990a09a 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -27,6 +27,23 @@ var ( }, } + // DefaultEtcdConfig defines default values for Etcd configurations. + DefaultEtcdConfig = EtcdConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + Firing: EtcdPostConf{ + URL: "", + KeyAnnotation: "etcd_key", + ValueAnnotation: "etcd_value", + }, + Resolved: EtcdPostConf{ + URL: "", + KeyAnnotation: "etcd_key", + ValueAnnotation: "etcd_value", + }, + } + // DefaultEmailConfig defines default values for Email configurations. DefaultEmailConfig = EmailConfig{ NotifierConfig: NotifierConfig{ @@ -291,6 +308,40 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return checkOverflow(c.XXX, "webhook config") } +// EtcdAlertConf holds information needed to push alerts to etcd +type EtcdPostConf struct { + URL string `yaml:"url" json:"url"` + KeyAnnotation string `yaml:"key_annotation" json:"key_annotation"` + ValueAnnotation string `yaml:"value_annotation" json:"value_annotation"` +} + +// EtcdConfig configures notifications to an etcd KV store. +type EtcdConfig struct { + NotifierConfig `yaml:",inline" json:",inline"` + + Firing EtcdPostConf `yaml:"firing" json:"firing"` + Resolved EtcdPostConf `yaml:"resolved" json:"resolved"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline" json:"-"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *EtcdConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultEtcdConfig + type plain EtcdConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + if c.Firing.URL == "" { + return fmt.Errorf("missing firing URL in etcd config") + } + if c.Resolved.URL == "" { + return fmt.Errorf("missing resolved URL in etcd config") + } + return checkOverflow(c.XXX, "etcd config") +} + // OpsGenieConfig configures notifications via OpsGenie. type OpsGenieConfig struct { NotifierConfig `yaml:",inline" json:",inline"` diff --git a/notify/impl.go b/notify/impl.go index 03321c969f..31f302f690 100644 --- a/notify/impl.go +++ b/notify/impl.go @@ -104,6 +104,10 @@ func BuildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template) []I n := NewWebhook(c, tmpl) add("webhook", i, n, c) } + for i, c := range nc.EtcdConfigs { + n := NewEtcd(c, tmpl) + add("etcd", i, n, c) + } for i, c := range nc.EmailConfigs { n := NewEmail(c, tmpl) add("email", i, n, c) @@ -206,6 +210,99 @@ func (w *Webhook) retry(statusCode int) (bool, error) { return false, nil } +// EtcdAlertConf holds information needed to push alerts to etcd +type EtcdAlertConf struct { + URL string // etcd instance to post to + KeyAnnotation string // annotation holding the etcd key to put/delete + ValueAnnotation string // annotation holding the value to put +} + +// Etcd implements a Notifier for setting etcd keys +type Etcd struct { + // map "firing" or "resolved" status to conf + confs map[model.AlertStatus]EtcdAlertConf + tmpl *template.Template +} + +// NewEtcd returns a new Etcd notifier. +func NewEtcd(conf *config.EtcdConfig, t *template.Template) *Etcd { + return &Etcd{ + confs: map[model.AlertStatus]EtcdAlertConf{ + model.AlertFiring: EtcdAlertConf{ + URL: conf.Firing.URL, + KeyAnnotation: conf.Firing.KeyAnnotation, + ValueAnnotation: conf.Firing.ValueAnnotation, + }, + model.AlertResolved: EtcdAlertConf{ + URL: conf.Resolved.URL, + KeyAnnotation: conf.Resolved.KeyAnnotation, + ValueAnnotation: conf.Resolved.ValueAnnotation, + }, + }, + tmpl: t, + } +} + +// EtcdMessage defines the JSON object to send to etcd. +type EtcdMessage struct { + Key []byte `json:"key"` + Value []byte `json:"value"` +} + +// Notify implements the Notifier interface. +func (n *Etcd) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) { + data := n.tmpl.Data(receiverName(ctx), groupLabels(ctx), alerts...) + + conf, ok := n.confs[types.Alerts(alerts...).Status()] + if !ok { + return false, fmt.Errorf("missing etcd conf for status '%v'", data.Status) + } + + key, ok := data.CommonAnnotations[conf.KeyAnnotation] + if !ok { + return false, fmt.Errorf("etcd key annotation '%v' missing", conf.KeyAnnotation) + } + value, ok := data.CommonAnnotations[conf.ValueAnnotation] + if !ok { + return false, fmt.Errorf("etcd value annotation '%v' missing", conf.ValueAnnotation) + } + + msg := &EtcdMessage{ + Key: []byte(key), + Value: []byte(value), + } + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(msg); err != nil { + return false, err + } + + req, err := http.NewRequest("POST", conf.URL, &buf) + if err != nil { + return true, err + } + req.Header.Set("Content-Type", contentTypeJSON) + req.Header.Set("User-Agent", userAgentHeader) + + resp, err := ctxhttp.Do(ctx, http.DefaultClient, req) + if err != nil { + return true, err + } + resp.Body.Close() + + return n.retry(resp.StatusCode) +} + +func (n *Etcd) retry(statusCode int) (bool, error) { + // Etcd is assumed to respond with 2xx response codes on a successful + // request and 5xx response codes are assumed to be recoverable. + if statusCode/100 != 2 { + return (statusCode/100 == 5), fmt.Errorf("unexpected status code %v", statusCode) + } + + return false, nil +} + // Email implements a Notifier for email notifications. type Email struct { conf *config.EmailConfig