From d20d76d2b3e01fe7b503ae53f5d48cbbd112011f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorben=20G=C3=BCnther?= Date: Sat, 4 Feb 2023 08:26:23 +0100 Subject: [PATCH] single mode: Cache alert fingerprints If ntfy-alertmanager breaks alertmanager's grouping feature, it should take care of caching alerts on its own. --- cache.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 21 ++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 cache.go diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..72f0f6a --- /dev/null +++ b/cache.go @@ -0,0 +1,54 @@ +package main + +import ( + "sync" + "time" +) + +type fingerprint string + +type cachedAlert struct { + expires time.Time +} + +type cache struct { + mu sync.Mutex + alerts map[fingerprint]*cachedAlert +} + +func (a *cachedAlert) expired() bool { + return a.expires.Before(time.Now()) +} + +func newCache() *cache { + c := new(cache) + c.alerts = make(map[fingerprint]*cachedAlert) + + return c +} + +func (c *cache) cleanup() { + c.mu.Lock() + defer c.mu.Unlock() + for key, value := range c.alerts { + if value.expired() { + delete(c.alerts, key) + } + } +} + +func (c *cache) set(f fingerprint, d time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + alert := new(cachedAlert) + alert.expires = time.Now().Add(d) + + c.alerts[f] = alert +} + +func (c *cache) contains(f fingerprint) bool { + c.mu.Lock() + defer c.mu.Unlock() + _, ok := c.alerts[f] + return ok +} diff --git a/main.go b/main.go index b4ad29c..9e00fac 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ var version string type receiver struct { cfg *config logger *log.Logger + cache *cache } type payload struct { @@ -40,6 +41,7 @@ type alert struct { Status string `json:"status"` Labels map[string]interface{} `json:"labels"` Annotations map[string]interface{} `json:"annotations"` + Fingerprint fingerprint `json:"fingerprint"` } type notification struct { @@ -52,6 +54,13 @@ type notification struct { func (rcv *receiver) singleAlertNotifications(p *payload) []*notification { var notifications []*notification for _, alert := range p.Alerts { + if rcv.cache.contains(alert.Fingerprint) { + rcv.logger.Debugf("Alert %s skipped: Still in cache", alert.Fingerprint) + continue + } + // TODO: Make configurable + rcv.cache.set(alert.Fingerprint, time.Hour*24) + n := new(notification) // create title @@ -275,6 +284,15 @@ func (rcv *receiver) basicAuthMiddleware(handler http.HandlerFunc) http.HandlerF } } +func (rcv *receiver) runCleanup() { + for { + // TODO: Make configurable + time.Sleep(time.Hour) + rcv.logger.Info("Pruning cache") + rcv.cache.cleanup() + } +} + func main() { var configPath string flag.StringVar(&configPath, "config", "/etc/ntfy-alertmanager/config", "config file path") @@ -298,7 +316,7 @@ func main() { logger.Errorf("config: %v", err) } - receiver := &receiver{cfg: cfg, logger: logger} + receiver := &receiver{cfg: cfg, logger: logger, cache: newCache()} logger.Infof("Listening on %s, ntfy-alertmanager %s", cfg.HTTPAddress, version) @@ -309,5 +327,6 @@ func main() { http.HandleFunc("/", receiver.handleWebhooks) } + go receiver.runCleanup() logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil)) }