diff --git a/main.go b/main.go index c7cd73e..b96d1f0 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ import ( var version = "dev" -type receiver struct { +type bridge struct { cfg *config logger *log.Logger cache *cache @@ -55,11 +55,11 @@ type notification struct { status string } -func (rcv *receiver) singleAlertNotifications(p *payload) []*notification { +func (br *bridge) singleAlertNotifications(p *payload) []*notification { var notifications []*notification for _, alert := range p.Alerts { - if rcv.cache.contains(alert.Fingerprint, status(alert.Status)) { - rcv.logger.Debugf("Alert %s skipped: Still in cache", alert.Fingerprint) + if br.cache.contains(alert.Fingerprint, status(alert.Status)) { + br.logger.Debugf("Alert %s skipped: Still in cache", alert.Fingerprint) continue } @@ -90,17 +90,17 @@ func (rcv *receiver) singleAlertNotifications(p *payload) []*notification { var tags []string if alert.Status == "resolved" { - tags = append(tags, rcv.cfg.resolved.Tags...) - n.icon = rcv.cfg.resolved.Icon + tags = append(tags, br.cfg.resolved.Tags...) + n.icon = br.cfg.resolved.Icon } - for _, labelName := range rcv.cfg.labels.Order { + for _, labelName := range br.cfg.labels.Order { val, ok := alert.Labels[labelName] if !ok { continue } - labelConfig, ok := rcv.cfg.labels.Label[fmt.Sprintf("%s:%s", labelName, val)] + labelConfig, ok := br.cfg.labels.Label[fmt.Sprintf("%s:%s", labelName, val)] if !ok { continue } @@ -122,9 +122,9 @@ func (rcv *receiver) singleAlertNotifications(p *payload) []*notification { n.tags = strings.Join(tags, ",") - if rcv.cfg.am.SilenceDuration != 0 && alert.Status == "firing" { - if rcv.cfg.BaseURL == "" { - rcv.logger.Error("Failed to create silence action: No base-url set") + if br.cfg.am.SilenceDuration != 0 && alert.Status == "firing" { + if br.cfg.BaseURL == "" { + br.logger.Error("Failed to create silence action: No base-url set") } else { // I could not convince ntfy to accept an Action with a body which contains // a json with more than one key. Instead the json will be base64 encoded @@ -133,7 +133,7 @@ func (rcv *receiver) singleAlertNotifications(p *payload) []*notification { s := &silenceBody{AlertManagerURL: p.ExternalURL, Labels: alert.Labels} b, err := json.Marshal(s) if err != nil { - rcv.logger.Errorf("Failed to create silence action: %v", err) + br.logger.Errorf("Failed to create silence action: %v", err) } n.silenceBody = base64.StdEncoding.EncodeToString(b) @@ -146,7 +146,7 @@ func (rcv *receiver) singleAlertNotifications(p *payload) []*notification { return notifications } -func (rcv *receiver) multiAlertNotification(p *payload) *notification { +func (br *bridge) multiAlertNotification(p *payload) *notification { n := new(notification) // create title @@ -186,17 +186,17 @@ func (rcv *receiver) multiAlertNotification(p *payload) *notification { var priority string var tags []string if p.Status == "resolved" { - tags = append(tags, rcv.cfg.resolved.Tags...) - n.icon = rcv.cfg.resolved.Icon + tags = append(tags, br.cfg.resolved.Tags...) + n.icon = br.cfg.resolved.Icon } - for _, labelName := range rcv.cfg.labels.Order { + for _, labelName := range br.cfg.labels.Order { val, ok := p.CommonLabels[labelName] if !ok { continue } - labelConfig, ok := rcv.cfg.labels.Label[fmt.Sprintf("%s:%s", labelName, val)] + labelConfig, ok := br.cfg.labels.Label[fmt.Sprintf("%s:%s", labelName, val)] if !ok { continue } @@ -220,15 +220,15 @@ func (rcv *receiver) multiAlertNotification(p *payload) *notification { n.priority = priority n.tags = tagString - if rcv.cfg.am.SilenceDuration != 0 && p.Status == "firing" { - if rcv.cfg.BaseURL == "" { - rcv.logger.Error("Failed to create silence action: No base-url set") + if br.cfg.am.SilenceDuration != 0 && p.Status == "firing" { + if br.cfg.BaseURL == "" { + br.logger.Error("Failed to create silence action: No base-url set") } else { s := &silenceBody{AlertManagerURL: p.ExternalURL, Labels: p.CommonLabels} b, err := json.Marshal(s) if err != nil { - rcv.logger.Errorf("Failed to create silence action: %v", err) + br.logger.Errorf("Failed to create silence action: %v", err) } n.silenceBody = base64.StdEncoding.EncodeToString(b) @@ -238,18 +238,18 @@ func (rcv *receiver) multiAlertNotification(p *payload) *notification { return n } -func (rcv *receiver) publish(n *notification) error { - req, err := http.NewRequest(http.MethodPost, rcv.cfg.ntfy.Topic, strings.NewReader(n.body)) +func (br *bridge) publish(n *notification) error { + req, err := http.NewRequest(http.MethodPost, br.cfg.ntfy.Topic, strings.NewReader(n.body)) if err != nil { return err } // ntfy authentication - if rcv.cfg.ntfy.Password != "" && rcv.cfg.ntfy.User != "" { - auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", rcv.cfg.ntfy.User, rcv.cfg.ntfy.Password))) + if br.cfg.ntfy.Password != "" && br.cfg.ntfy.User != "" { + auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", br.cfg.ntfy.User, br.cfg.ntfy.Password))) req.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth)) - } else if rcv.cfg.ntfy.AccessToken != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", rcv.cfg.ntfy.AccessToken)) + } else if br.cfg.ntfy.AccessToken != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", br.cfg.ntfy.AccessToken)) } req.Header.Set("X-Title", n.title) @@ -267,18 +267,18 @@ func (rcv *receiver) publish(n *notification) error { } if n.silenceBody != "" { - url := rcv.cfg.BaseURL + "/silences" + url := br.cfg.BaseURL + "/silences" var authString string - if rcv.cfg.User != "" && rcv.cfg.Password != "" { - auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", rcv.cfg.User, rcv.cfg.Password))) + if br.cfg.User != "" && br.cfg.Password != "" { + auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", br.cfg.User, br.cfg.Password))) authString = fmt.Sprintf(", headers.Authorization=Basic %s", auth) } req.Header.Set("Actions", fmt.Sprintf("http, Silence, %s, method=POST, body=%s%s", url, n.silenceBody, authString)) } - resp, err := rcv.client.Do(req) + resp, err := br.client.Do(req) if err != nil { return err } @@ -291,70 +291,70 @@ func (rcv *receiver) publish(n *notification) error { return nil } -func (rcv *receiver) handleWebhooks(w http.ResponseWriter, r *http.Request) { +func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if r.Method != http.MethodPost { http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) - rcv.logger.Debugf("illegal HTTP method: expected %q, got %q", "POST", r.Method) + br.logger.Debugf("illegal HTTP method: expected %q, got %q", "POST", r.Method) return } contentType := r.Header.Get("Content-Type") if contentType != "application/json" { http.Error(w, "Only application/json allowed", http.StatusUnsupportedMediaType) - rcv.logger.Debugf("illegal content type: %s", contentType) + br.logger.Debugf("illegal content type: %s", contentType) return } var event payload if err := json.NewDecoder(r.Body).Decode(&event); err != nil { - rcv.logger.Debug(err) + br.logger.Debug(err) return } - if rcv.logger.Level() == log.Debug { - rcv.logger.Debugf("Received alert %+v", event) + if br.logger.Level() == log.Debug { + br.logger.Debugf("Received alert %+v", event) } - if rcv.cfg.alertMode == single { - notifications := rcv.singleAlertNotifications(&event) + if br.cfg.alertMode == single { + notifications := br.singleAlertNotifications(&event) for _, n := range notifications { - err := rcv.publish(n) + err := br.publish(n) if err != nil { - rcv.logger.Errorf("Failed to publish notification: %v", err) + br.logger.Errorf("Failed to publish notification: %v", err) } else { - rcv.cache.set(n.fingerprint, status(n.status)) + br.cache.set(n.fingerprint, status(n.status)) } } } else { - notification := rcv.multiAlertNotification(&event) - err := rcv.publish(notification) + notification := br.multiAlertNotification(&event) + err := br.publish(notification) if err != nil { - rcv.logger.Errorf("Failed to publish notification: %v", err) + br.logger.Errorf("Failed to publish notification: %v", err) } } } -func (rcv *receiver) basicAuthMiddleware(handler http.HandlerFunc) http.HandlerFunc { +func (br *bridge) basicAuthMiddleware(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() if !ok { - rcv.logger.Debug("basic auth failure") + br.logger.Debug("basic auth failure") return } inputUserHash := sha512.Sum512([]byte(user)) inputPassHash := sha512.Sum512([]byte(pass)) - configUserHash := sha512.Sum512([]byte(rcv.cfg.User)) - configPassHash := sha512.Sum512([]byte(rcv.cfg.Password)) + configUserHash := sha512.Sum512([]byte(br.cfg.User)) + configPassHash := sha512.Sum512([]byte(br.cfg.Password)) validUser := subtle.ConstantTimeCompare(inputUserHash[:], configUserHash[:]) validPass := subtle.ConstantTimeCompare(inputPassHash[:], configPassHash[:]) if validUser != 1 || validPass != 1 { http.Error(w, "Unauthorized", http.StatusUnauthorized) - rcv.logger.Debug("basic auth: wrong user or password") + br.logger.Debug("basic auth: wrong user or password") return } @@ -362,11 +362,11 @@ func (rcv *receiver) basicAuthMiddleware(handler http.HandlerFunc) http.HandlerF } } -func (rcv *receiver) runCleanup() { +func (br *bridge) runCleanup() { for { - time.Sleep(rcv.cfg.cache.CleanupInterval) - rcv.logger.Info("Pruning cache") - rcv.cache.cleanup() + time.Sleep(br.cfg.cache.CleanupInterval) + br.logger.Info("Pruning cache") + br.cache.cleanup() } } @@ -395,19 +395,19 @@ func main() { client := &httpClient{&http.Client{Timeout: time.Second * 3}} - receiver := &receiver{cfg: cfg, logger: logger, cache: newCache(cfg.cache.Duration), client: client} + bridge := &bridge{cfg: cfg, logger: logger, cache: newCache(cfg.cache.Duration), client: client} logger.Infof("Listening on %s, ntfy-alertmanager %s", cfg.HTTPAddress, version) if cfg.User != "" && cfg.Password != "" { logger.Info("Enabling HTTP Basic Authentication") - http.HandleFunc("/", receiver.basicAuthMiddleware(receiver.handleWebhooks)) - http.HandleFunc("/silences", receiver.basicAuthMiddleware(receiver.handleSilences)) + http.HandleFunc("/", bridge.basicAuthMiddleware(bridge.handleWebhooks)) + http.HandleFunc("/silences", bridge.basicAuthMiddleware(bridge.handleSilences)) } else { - http.HandleFunc("/", receiver.handleWebhooks) - http.HandleFunc("/silences", receiver.handleSilences) + http.HandleFunc("/", bridge.handleWebhooks) + http.HandleFunc("/silences", bridge.handleSilences) } - go receiver.runCleanup() + go bridge.runCleanup() logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil)) } diff --git a/silence.go b/silence.go index 34c4a40..553a22c 100644 --- a/silence.go +++ b/silence.go @@ -36,31 +36,31 @@ type silenceResponse struct { ID string `json:"silenceID"` } -func (rcv *receiver) handleSilences(w http.ResponseWriter, r *http.Request) { +func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if r.Method != http.MethodPost { http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) - rcv.logger.Debugf("silences: illegal HTTP method: expected %q, got %q", "POST", r.Method) + br.logger.Debugf("silences: illegal HTTP method: expected %q, got %q", "POST", r.Method) return } b, err := io.ReadAll(r.Body) if err != nil { - rcv.logger.Debugf("silences: %v", err) + br.logger.Debugf("silences: %v", err) return } b, err = base64.StdEncoding.DecodeString(string(b)) if err != nil { - rcv.logger.Debugf("silences: %v", err) + br.logger.Debugf("silences: %v", err) return } var sb silenceBody err = json.Unmarshal(b, &sb) if err != nil { - rcv.logger.Debugf("silences: %v", err) + br.logger.Debugf("silences: %v", err) return } @@ -78,7 +78,7 @@ func (rcv *receiver) handleSilences(w http.ResponseWriter, r *http.Request) { silence := &silence{ StartsAt: time.Now().UTC().Format(dateLayout), - EndsAt: time.Now().Add(rcv.cfg.am.SilenceDuration).UTC().Format(dateLayout), + EndsAt: time.Now().Add(br.cfg.am.SilenceDuration).UTC().Format(dateLayout), CreatedBy: "ntfy-alertmanager", Comment: "", Matchers: matchers, @@ -86,52 +86,52 @@ func (rcv *receiver) handleSilences(w http.ResponseWriter, r *http.Request) { b, err = json.Marshal(silence) if err != nil { - rcv.logger.Debugf("silences: %v", err) + br.logger.Debugf("silences: %v", err) return } url := sb.AlertManagerURL - if rcv.cfg.am.URL != "" { - url = rcv.cfg.am.URL + if br.cfg.am.URL != "" { + url = br.cfg.am.URL } url += "/api/v2/silences" req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(b)) if err != nil { - rcv.logger.Debugf("silences: %v", err) + br.logger.Debugf("silences: %v", err) return } // Basic auth - if rcv.cfg.am.User != "" && rcv.cfg.am.Password != "" { - auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", rcv.cfg.am.User, rcv.cfg.am.Password))) + if br.cfg.am.User != "" && br.cfg.am.Password != "" { + auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", br.cfg.am.User, br.cfg.am.Password))) req.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth)) } req.Header.Add("Content-Type", "application/json") - resp, err := rcv.client.Do(req) + resp, err := br.client.Do(req) if err != nil { - rcv.logger.Debugf("silences: %v", err) + br.logger.Debugf("silences: %v", err) return } defer resp.Body.Close() b, err = io.ReadAll(resp.Body) if err != nil { - rcv.logger.Debugf("silences: %v", err) + br.logger.Debugf("silences: %v", err) return } if resp.StatusCode != http.StatusOK { - rcv.logger.Debugf("silences: received status code %d", resp.StatusCode) + br.logger.Debugf("silences: received status code %d", resp.StatusCode) return } var id silenceResponse if err := json.Unmarshal(b, &id); err != nil { - rcv.logger.Debugf("silences: %v", err) + br.logger.Debugf("silences: %v", err) return } - rcv.logger.Debugf("silences: created new silence %s", id.ID) + br.logger.Debugf("silences: created new silence %s", id.ID) }