diff --git a/main.go b/main.go index 106dc7e0..48927206 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,8 @@ package main import ( "flag" + "fmt" + "net" "os" "strings" "time" @@ -31,9 +33,27 @@ var ( configFile = flag.String("config.file", "alertmanager.conf", "Alert Manager configuration file name.") silencesFile = flag.String("silences.file", "silences.json", "Silence storage file name.") minRefreshPeriod = flag.Duration("alerts.min-refresh-period", 5*time.Minute, "Minimum required alert refresh period before an alert is purged.") + listenAddress = flag.String("web.listen-address", ":9093", "Address to listen on for the web interface and API.") pathPrefix = flag.String("web.path-prefix", "/", "Prefix for all web paths.") + hostname = flag.String("web.hostname", "", "Hostname on which the Alertmanager is available to the outside world.") ) +func alertmanagerURL(hostname, pathPrefix, addr string) (string, error) { + var err error + if hostname == "" { + hostname, err = os.Hostname() + if err != nil { + return "", err + } + } + + _, port, err := net.SplitHostPort(addr) + if err != nil { + return "", err + } + return fmt.Sprintf("http://%s:%s%s", hostname, port, pathPrefix), nil +} + func main() { flag.Parse() @@ -65,7 +85,11 @@ func main() { }() defer saveSilencesTicker.Stop() - notifier := manager.NewNotifier(conf.NotificationConfig) + amURL, err := alertmanagerURL(*hostname, *pathPrefix, *listenAddress) + if err != nil { + log.Fatalln("Error building Alertmanager URL:", err) + } + notifier := manager.NewNotifier(conf.NotificationConfig, amURL) defer notifier.Close() inhibitor := new(manager.Inhibitor) @@ -113,7 +137,7 @@ func main() { }, StatusHandler: statusHandler, } - go webService.ServeForever(*pathPrefix) + go webService.ServeForever(*listenAddress, *pathPrefix) // React to configuration changes. watcher := config.NewFileWatcher(*configFile) diff --git a/manager/alert.go b/manager/alert.go index a3f4dd81..2f39ca51 100644 --- a/manager/alert.go +++ b/manager/alert.go @@ -36,6 +36,8 @@ type Alert struct { Summary string `json:"summary"` // Long description of alert. Description string `json:"description"` + // Runbook link or reference for the alert. + Runbook string `json:"runbook"` // Label value pairs for purpose of aggregation, matching, and disposition // dispatching. This must minimally include an "alertname" label. Labels AlertLabelSet `json:"labels"` diff --git a/manager/notifier.go b/manager/notifier.go index 99101ed1..7f413fa1 100644 --- a/manager/notifier.go +++ b/manager/notifier.go @@ -51,6 +51,9 @@ Subject: [{{ .Status }}] {{.Alert.Labels.alertname}}: {{.Alert.Summary}} {{.Alert.Description}} +Alertmanager: {{.AlertmanagerURL}} +{{if .Alert.Runbook}}Runbook entry: {{.Alert.Runbook}}{{end}} + Grouping labels: {{range $label, $value := .Alert.Labels}} {{$label}} = "{{$value}}"{{end}} @@ -95,6 +98,8 @@ type notificationReq struct { type notifier struct { // Notifications that are queued to be sent. pendingNotifications chan *notificationReq + // URL that points back to this Alertmanager instance. + alertmanagerURL string // Mutex to protect the fields below. mu sync.Mutex @@ -103,9 +108,10 @@ type notifier struct { } // NewNotifier construct a new notifier. -func NewNotifier(configs []*pb.NotificationConfig) *notifier { +func NewNotifier(configs []*pb.NotificationConfig, amURL string) *notifier { notifier := ¬ifier{ pendingNotifications: make(chan *notificationReq, *notificationBufferSize), + alertmanagerURL: amURL, } notifier.SetNotificationConfigs(configs) return notifier @@ -156,9 +162,13 @@ func (n *notifier) sendPagerDutyNotification(serviceKey string, op notificationO "event_type": eventType, "description": a.Description, "incident_key": incidentKey, + "client": "Prometheus Alertmanager", + "client_url": n.alertmanagerURL, "details": map[string]interface{}{ "grouping_labels": a.Labels, "extra_labels": a.Payload, + "runbook": a.Runbook, + "summary": a.Summary, }, }) if err != nil { @@ -425,23 +435,25 @@ func postJSON(jsonMessage []byte, url string) (*http.Response, error) { return client.Post(url, contentTypeJSON, bytes.NewBuffer(jsonMessage)) } -func writeEmailBody(w io.Writer, from, to, status string, a *Alert) error { - return writeEmailBodyWithTime(w, from, to, status, a, time.Now()) +func writeEmailBody(w io.Writer, from, to, status string, a *Alert, amURL string) error { + return writeEmailBodyWithTime(w, from, to, status, a, time.Now(), amURL) } -func writeEmailBodyWithTime(w io.Writer, from, to, status string, a *Alert, moment time.Time) error { +func writeEmailBodyWithTime(w io.Writer, from, to, status string, a *Alert, moment time.Time, amURL string) error { err := bodyTmpl.Execute(w, struct { - From string - To string - Date string - Alert *Alert - Status string + From string + To string + Date string + Alert *Alert + Status string + AlertmanagerURL string }{ - From: from, - To: to, - Date: moment.Format("Mon, 2 Jan 2006 15:04:05 -0700"), - Alert: a, - Status: status, + From: from, + To: to, + Date: moment.Format("Mon, 2 Jan 2006 15:04:05 -0700"), + Alert: a, + Status: status, + AlertmanagerURL: amURL, }) if err != nil { return err @@ -529,7 +541,7 @@ func (n *notifier) sendEmailNotification(to string, op notificationOp, a *Alert) } defer wc.Close() - return writeEmailBody(wc, *smtpSender, to, status, a) + return writeEmailBody(wc, *smtpSender, to, status, a, n.alertmanagerURL) } func (n *notifier) sendPushoverNotification(token string, op notificationOp, userKey string, a *Alert) error { diff --git a/manager/notifier_test.go b/manager/notifier_test.go index e538b56a..5e6872b9 100644 --- a/manager/notifier_test.go +++ b/manager/notifier_test.go @@ -33,6 +33,7 @@ func TestWriteEmailBody(t *testing.T) { event := &Alert{ Summary: "Testsummary", Description: "Test alert description, something went wrong here.", + Runbook: "http://runbook/", Labels: AlertLabelSet{ "alertname": "TestAlert", "grouping_label1": "grouping_value1", @@ -46,7 +47,7 @@ func TestWriteEmailBody(t *testing.T) { buf := &bytes.Buffer{} location, _ := time.LoadLocation("Europe/Amsterdam") moment := time.Date(1918, 11, 11, 11, 0, 3, 0, location) - writeEmailBodyWithTime(buf, "from@prometheus.io", "to@prometheus.io", "ALERT", event, moment) + writeEmailBodyWithTime(buf, "from@prometheus.io", "to@prometheus.io", "ALERT", event, moment, "http://alertmanager/") expected := `From: Prometheus Alertmanager To: to@prometheus.io @@ -55,6 +56,9 @@ Subject: [ALERT] TestAlert: Testsummary Test alert description, something went wrong here. +Alertmanager: http://alertmanager/ +Runbook entry: http://runbook/ + Grouping labels: alertname = "TestAlert" diff --git a/web/web.go b/web/web.go index 1b93e5ff..4b4b9f9e 100644 --- a/web/web.go +++ b/web/web.go @@ -30,7 +30,6 @@ import ( // Commandline flags. var ( - listenAddress = flag.String("web.listen-address", ":9093", "Address to listen on for the web interface and API.") useLocalAssets = flag.Bool("web.use-local-assets", false, "Serve assets and templates from local files instead of from the binary.") ) @@ -41,8 +40,7 @@ type WebService struct { StatusHandler *StatusHandler } -func (w WebService) ServeForever(pathPrefix string) error { - +func (w WebService) ServeForever(addr string, pathPrefix string) error { http.Handle(pathPrefix+"favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "", 404) })) @@ -75,9 +73,9 @@ func (w WebService) ServeForever(pathPrefix string) error { } http.Handle(pathPrefix+"api/", w.AlertManagerService.Handler()) - log.Info("listening on ", *listenAddress) + log.Info("listening on ", addr) - return http.ListenAndServe(*listenAddress, nil) + return http.ListenAndServe(addr, nil) } func getLocalTemplate(name string, pathPrefix string) (*template.Template, error) {