diff --git a/config/config.go b/config/config.go index 3028992b47..db96e5b523 100644 --- a/config/config.go +++ b/config/config.go @@ -90,6 +90,7 @@ type Config struct { Route *Route `yaml:"route,omitempty"` InhibitRules []*InhibitRule `yaml:"inhibit_rules,omitempty"` Receivers []*Receiver `yaml:"receivers,omitempty"` + Heartbeats []*Heartbeat `yaml:"heartbeats,omitempty"` Templates []string `yaml:"templates"` // Catches all undefined fields and must be empty after parsing. @@ -231,6 +232,20 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { names[rcv.Name] = struct{}{} } + for _, hbts := range c.Heartbeats { + for _, ogc := range hbts.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 += "/" + } + } + } + // The root route must not have any matchers as it is the fallback node // for all alerts. if c.Route == nil { @@ -443,6 +458,29 @@ func (c *Receiver) UnmarshalYAML(unmarshal func(interface{}) error) error { return checkOverflow(c.XXX, "receiver config") } +// Heartbeat configuration provides configuration on how to ping a heartbeat. +type Heartbeat struct { + // A unique identifier for this heartbeat. + Name string `yaml:"name"` + + OpsGenieConfigs []*HeartbeatOpsGenieConfig `yaml:"opsgenie_configs,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *Heartbeat) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain Heartbeat + if err := unmarshal((*plain)(c)); err != nil { + return err + } + if c.Name == "" { + return fmt.Errorf("missing name in heartbeat") + } + return checkOverflow(c.XXX, "heartbeat config") +} + // Regexp encapsulates a regexp.Regexp and makes it YAML marshalable. type Regexp struct { *regexp.Regexp diff --git a/config/heartbeats.go b/config/heartbeats.go new file mode 100644 index 0000000000..3b4789b02e --- /dev/null +++ b/config/heartbeats.go @@ -0,0 +1,52 @@ +// Copyright 2015 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "fmt" + "time" +) + +var ( + DefaultHeartbeatOpsGenieConfig = HeartbeatOpsGenieConfig{ + Interval: duration(1 * time.Minute), + } +) + +// HeartbeatOpsGenieConfig configures heartbeats to OpsGenie. +type HeartbeatOpsGenieConfig struct { + APIKey Secret `yaml:"api_key"` + APIHost string `yaml:"api_host"` + Name string `yaml:"name"` + Interval duration `yaml:"interval,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *HeartbeatOpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultHeartbeatOpsGenieConfig + type plain HeartbeatOpsGenieConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + if c.Name == "" { + return fmt.Errorf("missing hearbeat name in OpsGenie heartbeat config") + } + if c.APIKey == "" { + return fmt.Errorf("missing API key in OpsGenie heartbeat config") + } + return checkOverflow(c.XXX, "opsgenie heartbeat config") +} diff --git a/heartbeat/heartbeat.go b/heartbeat/heartbeat.go new file mode 100644 index 0000000000..6d82b5cc8e --- /dev/null +++ b/heartbeat/heartbeat.go @@ -0,0 +1,83 @@ +// Copyright 2016 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package heartbeat + +import ( + "time" + + "github.com/prometheus/common/log" +) + +type Fanout map[string]Heartbeat + +type Heartbeat interface { + Interval() time.Duration + SendHeartbeat() error +} + +// NewHeartbeatRunner returns a new HeartbeatRunner that runs a list of +// HeartbeatSender. +type HeartbeatRunner struct { + senders map[string]Fanout + done map[string]chan struct{} + + log log.Logger +} + +// NewHeartbeatSender returns a new HeartbeatSender. +func NewHeartbeatRunner(senders map[string]Fanout) *HeartbeatRunner { + runner := &HeartbeatRunner{ + senders: senders, + log: log.With("component", "heartbeat_runner"), + done: make(map[string]chan struct{}), + } + return runner +} + +func (r *HeartbeatRunner) Run() { + for _, t := range r.senders { + for k, s := range t { + r.done[k] = make(chan struct{}) + go func(h Heartbeat, done chan struct{}) { + var err error + c := time.NewTicker(h.Interval()) + defer c.Stop() + + for { + select { + case <-c.C: + err = h.SendHeartbeat() + if err != nil { + log.Error(err) + } + + case <-done: + return + } + } + }(s, r.done[k]) + } + } +} + +func (r *HeartbeatRunner) Stop() { + if r == nil { + return + } + for _, t := range r.senders { + for k, _ := range t { + close(r.done[k]) + } + } +} diff --git a/heartbeat/impl.go b/heartbeat/impl.go new file mode 100644 index 0000000000..b8991861ed --- /dev/null +++ b/heartbeat/impl.go @@ -0,0 +1,106 @@ +// Copyright 2016 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package heartbeat + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/common/log" +) + +type integration interface { + Heartbeat + name() string +} + +const contentTypeJSON = "application/json" + +func Build(confs []*config.Heartbeat) map[string]Fanout { + res := map[string]Fanout{} + + for _, nc := range confs { + var ( + hb = Fanout{} + add = func(i int, on integration) { hb[fmt.Sprintf("%s/%d", on.name(), i)] = on } + ) + + for i, c := range nc.OpsGenieConfigs { + n := NewOpsGenie(c) + add(i, n) + } + res[nc.Name] = hb + } + return res + +} + +// OpsGenie represents a OpsGenie implementation of Heartbeat +type OpsGenie struct { + conf *config.HeartbeatOpsGenieConfig + done chan struct{} + + log log.Logger +} + +// Returns a new OpsGenie object +func NewOpsGenie(conf *config.HeartbeatOpsGenieConfig) *OpsGenie { + return &OpsGenie{ + conf: conf, + done: make(chan struct{}), + log: log.With("component", "opsgenie_heartbeat"), + } +} + +func (o *OpsGenie) name() string { return fmt.Sprintf("[%s] %s", "opsgenie", o.conf.Name) } + +// Represents an OpsGenie heartbeat message +type opsGenieHeartBeatMessage struct { + APIKey string `json:"apiKey"` + Name string `json:"name"` +} + +func (o *OpsGenie) Interval() time.Duration { + return time.Duration(o.conf.Interval) +} + +func (o *OpsGenie) SendHeartbeat() error { + var msg = &opsGenieHeartBeatMessage{ + APIKey: string(o.conf.APIKey), + Name: o.conf.Name, + } + apiURL := o.conf.APIHost + "v1/json/heartbeat/send" + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(msg); err != nil { + return err + } + log.Debugf("Sending heartbeat to %s: %s", o.conf.APIHost, buf.String()) + + resp, err := http.Post(apiURL, contentTypeJSON, &buf) + if err != nil { + return err + } + resp.Body.Close() + + if resp.StatusCode/100 != 2 { + return fmt.Errorf("unexpected status code %v from %s", resp.StatusCode, apiURL) + } + + return nil +} diff --git a/main.go b/main.go index 2ac77b3d6d..8722bbdfb8 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ import ( "github.com/prometheus/common/version" "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/heartbeat" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/provider/boltmem" "github.com/prometheus/alertmanager/template" @@ -104,9 +105,10 @@ func main() { defer silences.Close() var ( - inhibitor *Inhibitor - tmpl *template.Template - disp *Dispatcher + inhibitor *Inhibitor + tmpl *template.Template + disp *Dispatcher + heartbeatRunner *heartbeat.HeartbeatRunner ) defer disp.Stop() @@ -146,6 +148,10 @@ func main() { log.Fatal(err) } + heartbeat_builder := func(hrbts []*config.Heartbeat) *heartbeat.HeartbeatRunner { + return heartbeat.NewHeartbeatRunner(heartbeat.Build(hrbts)) + } + reload := func() (err error) { log.With("file", *configFile).Infof("Loading configuration file") defer func() { @@ -173,12 +179,15 @@ func main() { inhibitor.Stop() disp.Stop() + heartbeatRunner.Stop() inhibitor = NewInhibitor(alerts, conf.InhibitRules, marker) disp = NewDispatcher(alerts, NewRoute(conf.Route, nil), build(conf.Receivers), marker) + heartbeatRunner = heartbeat_builder(conf.Heartbeats) go disp.Run() go inhibitor.Run() + heartbeatRunner.Run() return nil }