diff --git a/config/instrumentation.go b/config/instrumentation.go index dd6ea98b..9377163f 100644 --- a/config/instrumentation.go +++ b/config/instrumentation.go @@ -17,8 +17,27 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var configLoads = prometheus.NewCounter() +const namespace = "alertmanager" + +var configReloads = prometheus.NewCounter( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "config", + Name: "reloads_total", + Help: "The total number of configuration reloads.", + }, +) + +var failedConfigReloads = prometheus.NewCounter( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "config", + Name: "failed_reloads_total", + Help: "The number of failed configuration reloads.", + }, +) func init() { - prometheus.Register("alertmanager_config_reloads_total", "The number of configuration reloads.", prometheus.NilLabels, configLoads) + prometheus.MustRegister(configReloads) + prometheus.MustRegister(failedConfigReloads) } diff --git a/config/watcher.go b/config/watcher.go index 8cb8290e..e942d26b 100644 --- a/config/watcher.go +++ b/config/watcher.go @@ -52,11 +52,11 @@ func (w *fileWatcher) Watch(cb ReloadCallback) { conf, err := LoadFromFile(w.fileName) if err != nil { glog.Error("Error loading new config: ", err) - configLoads.Increment(map[string]string{"outcome": "failure"}) + failedConfigReloads.Inc() } else { cb(&conf) glog.Info("Config reloaded successfully") - configLoads.Increment(map[string]string{"outcome": "success"}) + configReloads.Inc() } // Re-add the file watcher since it can get lost on some changes. E.g. // saving a file with vim results in a RENAME-MODIFY-DELETE event diff --git a/manager/silencer.go b/manager/silencer.go index ed03f7ee..7a8e3f2b 100644 --- a/manager/silencer.go +++ b/manager/silencer.go @@ -23,12 +23,12 @@ import ( "github.com/golang/glog" ) -type SilenceId uint +type SilenceID uint type Silences []*Silence type Silence struct { // The numeric ID of the silence. - Id SilenceId + ID SilenceID // Name/email of the silence creator. CreatedBy string // When the silence was first created (Unix timestamp). @@ -45,7 +45,7 @@ type Silence struct { } type ApiSilence struct { - Id SilenceId + ID SilenceID CreatedBy string CreatedAtSeconds int64 EndsAtSeconds int64 @@ -62,7 +62,7 @@ func (s *Silence) MarshalJSON() ([]byte, error) { } return json.Marshal(&ApiSilence{ - Id: s.Id, + ID: s.ID, CreatedBy: s.CreatedBy, CreatedAtSeconds: s.CreatedAt.Unix(), EndsAtSeconds: s.EndsAt.Unix(), @@ -88,7 +88,7 @@ func (s *Silence) UnmarshalJSON(data []byte) error { } *s = Silence{ - Id: sc.Id, + ID: sc.ID, CreatedBy: sc.CreatedBy, CreatedAt: time.Unix(sc.CreatedAtSeconds, 0).UTC(), EndsAt: time.Unix(sc.EndsAtSeconds, 0).UTC(), @@ -104,9 +104,9 @@ func (s Silence) Matches(l AlertLabelSet) bool { type Silencer struct { // Silences managed by this Silencer. - Silences map[SilenceId]*Silence - // Used to track the next Silence Id to allocate. - lastId SilenceId + Silences map[SilenceID]*Silence + // Used to track the next Silence ID to allocate. + lastID SilenceID // Tracks whether silences have changed since the last call to HasChanged. dirty bool @@ -120,13 +120,13 @@ type IsSilencedInterrogator interface { func NewSilencer() *Silencer { return &Silencer{ - Silences: make(map[SilenceId]*Silence), + Silences: make(map[SilenceID]*Silence), } } -func (s *Silencer) nextSilenceId() SilenceId { - s.lastId++ - return s.lastId +func (s *Silencer) nextSilenceID() SilenceID { + s.lastID++ + return s.lastID } func (s *Silencer) setupExpiryTimer(sc *Silence) { @@ -135,29 +135,29 @@ func (s *Silencer) setupExpiryTimer(sc *Silence) { } expDuration := sc.EndsAt.Sub(time.Now()) sc.expiryTimer = time.AfterFunc(expDuration, func() { - if err := s.DelSilence(sc.Id); err != nil { - glog.Errorf("Failed to delete silence %d: %s", sc.Id, err) + if err := s.DelSilence(sc.ID); err != nil { + glog.Errorf("Failed to delete silence %d: %s", sc.ID, err) } }) } -func (s *Silencer) AddSilence(sc *Silence) SilenceId { +func (s *Silencer) AddSilence(sc *Silence) SilenceID { s.mu.Lock() defer s.mu.Unlock() s.dirty = true - if sc.Id == 0 { - sc.Id = s.nextSilenceId() + if sc.ID == 0 { + sc.ID = s.nextSilenceID() } else { - if sc.Id > s.lastId { - s.lastId = sc.Id + if sc.ID > s.lastID { + s.lastID = sc.ID } } s.setupExpiryTimer(sc) - s.Silences[sc.Id] = sc - return sc.Id + s.Silences[sc.ID] = sc + return sc.ID } func (s *Silencer) UpdateSilence(sc *Silence) error { @@ -166,9 +166,9 @@ func (s *Silencer) UpdateSilence(sc *Silence) error { s.dirty = true - origSilence, ok := s.Silences[sc.Id] + origSilence, ok := s.Silences[sc.ID] if !ok { - return fmt.Errorf("Silence with ID %d doesn't exist", sc.Id) + return fmt.Errorf("Silence with ID %d doesn't exist", sc.ID) } if sc.EndsAt != origSilence.EndsAt { origSilence.expiryTimer.Stop() @@ -178,7 +178,7 @@ func (s *Silencer) UpdateSilence(sc *Silence) error { return nil } -func (s *Silencer) GetSilence(id SilenceId) (*Silence, error) { +func (s *Silencer) GetSilence(id SilenceID) (*Silence, error) { s.mu.Lock() defer s.mu.Unlock() @@ -189,7 +189,7 @@ func (s *Silencer) GetSilence(id SilenceId) (*Silence, error) { return sc, nil } -func (s *Silencer) DelSilence(id SilenceId) error { +func (s *Silencer) DelSilence(id SilenceID) error { s.mu.Lock() defer s.mu.Unlock() diff --git a/manager/silencer_test.go b/manager/silencer_test.go index ebec972b..85efe1cc 100644 --- a/manager/silencer_test.go +++ b/manager/silencer_test.go @@ -33,10 +33,10 @@ func (scenario *testSilencerScenario) test(i int, t *testing.T) { if err != nil { t.Fatalf("%d.%d. Error getting silence: %s", i, j, err) } - if retrievedSilence.Id != id { - t.Fatalf("%d.%d. Expected ID %d, got %d", i, j, id, retrievedSilence.Id) + if retrievedSilence.ID != id { + t.Fatalf("%d.%d. Expected ID %d, got %d", i, j, id, retrievedSilence.ID) } - sc.Id = id + sc.ID = id if sc != retrievedSilence { t.Fatalf("%d.%d. Expected silence %v, got %v", i, j, sc, retrievedSilence) } @@ -76,7 +76,7 @@ func (scenario *testSilencerScenario) test(i int, t *testing.T) { } for j, sc := range silences { - if err := s.DelSilence(sc.Id); err != nil { + if err := s.DelSilence(sc.ID); err != nil { t.Fatalf("%d.%d. Got error while deleting silence: %s", i, j, err) } diff --git a/web/api/alert.go b/web/api/alert.go index 89c30253..ee875788 100644 --- a/web/api/alert.go +++ b/web/api/alert.go @@ -14,28 +14,29 @@ package api import ( + "fmt" "net/http" - "github.com/golang/glog" + "github.com/julienschmidt/httprouter" "github.com/prometheus/alertmanager/manager" ) -func (s AlertManagerService) AddAlerts(as manager.Alerts) { - for i, a := range as { +func (s AlertManagerService) addAlerts(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + alerts := manager.Alerts{} + if err := parseJSON(w, r, &alerts); err != nil { + return + } + for i, a := range alerts { if a.Summary == "" || a.Description == "" { - glog.Errorf("Missing field in alert %d: %s", i, a) - rb := s.ResponseBuilder() - rb.SetResponseCode(http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Missing field in alert %d: %s", i, a), http.StatusBadRequest) return } if _, ok := a.Labels[manager.AlertNameLabel]; !ok { - glog.Errorf("Missing alert name label in alert %d: %s", i, a) - rb := s.ResponseBuilder() - rb.SetResponseCode(http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Missing alert name label in alert %d: %s", i, a), http.StatusBadRequest) return } } - s.Manager.Receive(as) + s.Manager.Receive(alerts) } diff --git a/web/api/api.go b/web/api/api.go index 2ad767e9..8d82c55a 100644 --- a/web/api/api.go +++ b/web/api/api.go @@ -14,21 +14,54 @@ package api import ( - "code.google.com/p/gorest" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/julienschmidt/httprouter" "github.com/prometheus/alertmanager/manager" ) type AlertManagerService struct { - gorest.RestService `root:"/api/" consumes:"application/json" produces:"application/json"` - - addAlerts gorest.EndPoint `method:"POST" path:"/alerts" postdata:"Alerts"` - addSilence gorest.EndPoint `method:"POST" path:"/silences" postdata:"Silence"` - getSilence gorest.EndPoint `method:"GET" path:"/silences/{id:int}" output:"string"` - updateSilence gorest.EndPoint `method:"POST" path:"/silences/{id:int}" postdata:"Silence"` - delSilence gorest.EndPoint `method:"DELETE" path:"/silences/{id:int}"` - silenceSummary gorest.EndPoint `method:"GET" path:"/silences" output:"string"` - Manager manager.AlertManager Silencer *manager.Silencer } + +func (s AlertManagerService) Handler() http.Handler { + r := httprouter.New() + + r.POST("/api/alerts", s.addAlerts) + r.GET("/api/silences", s.silenceSummary) + r.POST("/api/silences", s.addSilence) + r.GET("/api/silences/:id", s.getSilence) + r.POST("/api/silences/:id", s.updateSilence) + r.DELETE("/api/silences/:id", s.deleteSilence) + + return r +} + +func respondJSON(w http.ResponseWriter, v interface{}) { + resultBytes, err := json.Marshal(v) + if err != nil { + http.Error(w, fmt.Sprint("Error marshalling JSON: ", err), http.StatusInternalServerError) + return + } + w.Header().Set("Content-type", "application/json") + w.Write(resultBytes) +} + +func getID(p httprouter.Params) int { + n, _ := strconv.Atoi(p.ByName("id")) + return n +} + +func parseJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { + d := json.NewDecoder(r.Body) + if err := d.Decode(v); err != nil { + http.Error(w, fmt.Sprint("failed to parse JSON: ", err.Error()), http.StatusBadRequest) + return err + } + return nil +} diff --git a/web/api/silence.go b/web/api/silence.go index b29a58e1..ac6e826d 100644 --- a/web/api/silence.go +++ b/web/api/silence.go @@ -14,80 +14,57 @@ package api import ( - "encoding/json" "fmt" "net/http" - "code.google.com/p/gorest" - "github.com/golang/glog" + //"github.com/golang/glog" + "github.com/julienschmidt/httprouter" "github.com/prometheus/alertmanager/manager" ) -type Silence struct { - CreatedBy string - CreatedAtSeconds int64 - EndsAtSeconds int64 - Comment string - Filters map[string]string -} - -func (s AlertManagerService) AddSilence(sc manager.Silence) { +func (s AlertManagerService) addSilence(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + sc := manager.Silence{} + if err := parseJSON(w, r, &sc); err != nil { + return + } // BUG: add server-side form validation. id := s.Silencer.AddSilence(&sc) - rb := s.ResponseBuilder() - rb.SetResponseCode(http.StatusCreated) - rb.Location(fmt.Sprintf("/api/silences/%d", id)) + w.WriteHeader(http.StatusCreated) + w.Header().Set("Location", fmt.Sprintf("/api/silences/%d", id)) } -func (s AlertManagerService) GetSilence(id int) string { - rb := s.ResponseBuilder() - rb.SetContentType(gorest.Application_Json) - silence, err := s.Silencer.GetSilence(manager.SilenceId(id)) +func (s AlertManagerService) getSilence(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + silence, err := s.Silencer.GetSilence(manager.SilenceID(getID(p))) if err != nil { - glog.Error("Error getting silence: ", err) - rb.SetResponseCode(http.StatusNotFound) - return err.Error() + http.Error(w, fmt.Sprint("Error getting silence: ", err), http.StatusNotFound) + return } - resultBytes, err := json.Marshal(&silence) - if err != nil { - glog.Error("Error marshalling silence: ", err) - rb.SetResponseCode(http.StatusInternalServerError) - return err.Error() - } - return string(resultBytes) + respondJSON(w, &silence) } -func (s AlertManagerService) UpdateSilence(sc manager.Silence, id int) { +func (s AlertManagerService) updateSilence(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + sc := manager.Silence{} + if err := parseJSON(w, r, &sc); err != nil { + return + } // BUG: add server-side form validation. - sc.Id = manager.SilenceId(id) + sc.ID = manager.SilenceID(getID(p)) if err := s.Silencer.UpdateSilence(&sc); err != nil { - glog.Error("Error updating silence: ", err) - rb := s.ResponseBuilder() - rb.SetResponseCode(http.StatusNotFound) + http.Error(w, fmt.Sprint("Error updating silence: ", err), http.StatusNotFound) + return } } -func (s AlertManagerService) DelSilence(id int) { - if err := s.Silencer.DelSilence(manager.SilenceId(id)); err != nil { - glog.Error("Error deleting silence: ", err) - rb := s.ResponseBuilder() - rb.SetResponseCode(http.StatusNotFound) +func (s AlertManagerService) deleteSilence(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + if err := s.Silencer.DelSilence(manager.SilenceID(getID(p))); err != nil { + http.Error(w, fmt.Sprint("Error deleting silence: ", err), http.StatusNotFound) + return } } -func (s AlertManagerService) SilenceSummary() string { - rb := s.ResponseBuilder() - rb.SetContentType(gorest.Application_Json) - silenceSummary := s.Silencer.SilenceSummary() - - resultBytes, err := json.Marshal(silenceSummary) - if err != nil { - glog.Error("Error marshalling silences: ", err) - rb.SetResponseCode(http.StatusInternalServerError) - return err.Error() - } - return string(resultBytes) +func (s AlertManagerService) silenceSummary(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + respondJSON(w, s.Silencer.SilenceSummary()) } diff --git a/web/static/js/alerts.js b/web/static/js/alerts.js index c64dc7d9..ab53a444 100644 --- a/web/static/js/alerts.js +++ b/web/static/js/alerts.js @@ -1,5 +1,5 @@ var silenceRow = null; -var silenceId = null; +var silenceID = null; function clearSilenceLabels() { $("#silence_filters_table").empty(); @@ -74,7 +74,7 @@ function createSilence() { function updateSilence() { $.ajax({ type: "POST", - url: "/api/silences/" + silenceId, + url: "/api/silences/" + silenceID, data: silenceJsonFromForm(), dataType: "text", success: function(data, textStatus, jqXHR) { @@ -86,22 +86,22 @@ function updateSilence() { }); } -function getSilence(silenceId, successFn) { +function getSilence(silenceID, successFn) { $.ajax({ type: "GET", - url: "/api/silences/" + silenceId, + url: "/api/silences/" + silenceID, async: false, success: successFn, error: function(data, textStatus, jqXHR) { - alert("Creating silence failed: " + textStatus); + alert("Getting silence failed: " + textStatus); } }); } -function deleteSilence(silenceId, silenceRow) { +function deleteSilence(silenceID, silenceRow) { $.ajax({ type: "DELETE", - url: "/api/silences/" + silenceId, + url: "/api/silences/" + silenceID, success: function(data, textStatus, jqXHR) { silenceRow.remove(); $("#del_silence_modal").modal("hide"); @@ -113,7 +113,7 @@ function deleteSilence(silenceId, silenceRow) { } function initNewSilence() { - silenceId = null; + silenceID = null; $("#edit_silence_header, #edit_silence_btn").html("Create Silence"); $("#edit_silence_form")[0].reset(); } @@ -162,7 +162,7 @@ function init() { }); $("#edit_silence_form").submit(function() { - if (silenceId != null) { + if (silenceID != null) { updateSilence(); } else { createSilence(); @@ -174,9 +174,9 @@ function init() { $("#edit_silence_header, #edit_silence_btn").html("Update Silence"); silenceRow = $(this).parents("tr"); - silenceId = silenceRow.find("input[name='silence_id']").val(); - $("#edit_silence_form input[name='silence_id']").val(silenceId); - getSilence(silenceId, function(silence) { + silenceID = silenceRow.find("input[name='silence_id']").val(); + $("#edit_silence_form input[name='silence_id']").val(silenceID); + getSilence(silenceID, function(silence) { var picker = $("#ends_at_datetimepicker").data('datetimepicker'); var endsAt = new Date(silence.EndsAtSeconds * 1000); picker.setLocalDate(endsAt); @@ -194,12 +194,12 @@ function init() { // from the modal dialog to remove that silence. $(".del_silence_modal_btn").click(function() { silenceRow = $(this).parents("tr"); - silenceId = silenceRow.find("input[name='silence_id']").val(); + silenceID = silenceRow.find("input[name='silence_id']").val(); }); // Deletion confirmation button action. $(".del_silence_btn").click(function() { - deleteSilence(silenceId, silenceRow); + deleteSilence(silenceID, silenceRow); }); $(".silence_link").click(function() { diff --git a/web/templates/alerts.html b/web/templates/alerts.html index 0cf05398..5f700391 100644 --- a/web/templates/alerts.html +++ b/web/templates/alerts.html @@ -46,12 +46,12 @@ {{timeSince .Created}} ago {{timeSince .LastRefreshed}} ago - {{(truncate .Alert.Payload.GeneratorUrl 40)}} + {{(truncate .Alert.Payload.GeneratorURL 40)}} {{.Alert.Payload.AlertingRule}} {{$silence := call $silenceForAlert .Alert}} {{if $silence}} - by silence {{$silence.Id}} + by silence {{$silence.ID}} {{else}} not silenced {{end}} diff --git a/web/templates/silences.html b/web/templates/silences.html index 59a5e596..bc0d2279 100644 --- a/web/templates/silences.html +++ b/web/templates/silences.html @@ -22,7 +22,7 @@ {{range .Silences}} - {{.Id}} + {{.ID}} {{range .Filters}} {{.NamePattern}}="{{.ValuePattern}}" @@ -33,7 +33,7 @@ {{.EndsAt}} {{.Comment}} - + Edit Silence Remove Silence diff --git a/web/web.go b/web/web.go index 174c84c0..59d63a15 100644 --- a/web/web.go +++ b/web/web.go @@ -18,12 +18,10 @@ import ( "fmt" "html/template" "net/http" - "net/http/pprof" + _ "net/http/pprof" - "code.google.com/p/gorest" "github.com/golang/glog" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/exp" "github.com/prometheus/alertmanager/web/api" "github.com/prometheus/alertmanager/web/blob" @@ -43,35 +41,27 @@ type WebService struct { } func (w WebService) ServeForever() error { - gorest.RegisterService(w.AlertManagerService) - exp.Handle("/favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Handle("/favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "", 404) })) - // TODO(julius): This will need to be rewritten once the exp package provides - // the coarse mux behaviors via a wrapper function. - exp.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) - exp.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) - exp.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) - exp.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) + http.Handle("/", prometheus.InstrumentHandler("index", w.AlertsHandler)) + http.Handle("/alerts", prometheus.InstrumentHandler("alerts", w.AlertsHandler)) + http.Handle("/silences", prometheus.InstrumentHandler("silences", w.SilencesHandler)) + http.Handle("/status", prometheus.InstrumentHandler("status", w.StatusHandler)) - exp.Handle("/", w.AlertsHandler) - exp.Handle("/alerts", w.AlertsHandler) - exp.Handle("/silences", w.SilencesHandler) - exp.Handle("/status", w.StatusHandler) - - exp.Handle("/api/", compressionHandler{handler: gorest.Handle()}) - exp.Handle("/metrics", prometheus.DefaultHandler) + http.Handle("/metrics", prometheus.Handler()) if *useLocalAssets { - exp.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static")))) + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static")))) } else { - exp.Handle("/static/", http.StripPrefix("/static/", new(blob.Handler))) + http.Handle("/static/", http.StripPrefix("/static/", new(blob.Handler))) } + http.Handle("/api/", w.AlertManagerService.Handler()) glog.Info("listening on ", *listenAddress) - return http.ListenAndServe(*listenAddress, exp.DefaultCoarseMux) + return http.ListenAndServe(*listenAddress, nil) } func getLocalTemplate(name string) (*template.Template, error) {