Update HTTP/clientlib dependencies + cleanups.

Change-Id: I175ac4874b25358dd569866e3d575ba49e4357f2
This commit is contained in:
Julius Volz 2014-10-27 14:53:31 +01:00
parent 6d789102f9
commit 20b146657b
11 changed files with 163 additions and 143 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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()

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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())
}

View File

@ -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() {

View File

@ -46,12 +46,12 @@
</td>
<td>{{timeSince .Created}} ago</td>
<td>{{timeSince .LastRefreshed}} ago</td>
<td><a href="{{.Alert.Payload.GeneratorUrl}}">{{(truncate .Alert.Payload.GeneratorUrl 40)}}</a></td>
<td><a href="{{.Alert.Payload.GeneratorURL}}">{{(truncate .Alert.Payload.GeneratorURL 40)}}</a></td>
<td>{{.Alert.Payload.AlertingRule}}</td>
<td>
{{$silence := call $silenceForAlert .Alert}}
{{if $silence}}
by <a href="#" class="silence_link">silence {{$silence.Id}}</a>
by <a href="#" class="silence_link">silence {{$silence.ID}}</a>
{{else}}
not silenced
{{end}}

View File

@ -22,7 +22,7 @@
<tbody>
{{range .Silences}}
<tr>
<td>{{.Id}}</td>
<td>{{.ID}}</td>
<td>
{{range .Filters}}
<span class="label label-info">{{.NamePattern}}="{{.ValuePattern}}"</span>
@ -33,7 +33,7 @@
<td>{{.EndsAt}}</td>
<td>{{.Comment}}</td>
<td>
<input type="hidden" name="silence_id" value="{{.Id}}">
<input type="hidden" name="silence_id" value="{{.ID}}">
<a href="#edit_silence_modal" role="button" class="btn btn-mini btn-primary edit_silence_btn" id="new_silence_btn" data-toggle="modal">Edit Silence</a>
<a href="#del_silence_modal" role="button" class="btn btn-mini btn-danger del_silence_modal_btn" data-toggle="modal">Remove Silence</a>

View File

@ -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) {