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 etcd notifier #1003

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
51 changes: 51 additions & 0 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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"`
Expand Down
97 changes: 97 additions & 0 deletions notify/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down