single mode: Cache alert fingerprints

If ntfy-alertmanager breaks alertmanager's grouping feature, it should
take care of caching alerts on its own.
This commit is contained in:
Thorben Günther 2023-02-04 08:26:23 +01:00
parent 8933ccd825
commit d20d76d2b3
No known key found for this signature in database
GPG key ID: 415CD778D8C5AFED
2 changed files with 74 additions and 1 deletions

54
cache.go Normal file
View file

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

21
main.go
View file

@ -26,6 +26,7 @@ var version string
type receiver struct { type receiver struct {
cfg *config cfg *config
logger *log.Logger logger *log.Logger
cache *cache
} }
type payload struct { type payload struct {
@ -40,6 +41,7 @@ type alert struct {
Status string `json:"status"` Status string `json:"status"`
Labels map[string]interface{} `json:"labels"` Labels map[string]interface{} `json:"labels"`
Annotations map[string]interface{} `json:"annotations"` Annotations map[string]interface{} `json:"annotations"`
Fingerprint fingerprint `json:"fingerprint"`
} }
type notification struct { type notification struct {
@ -52,6 +54,13 @@ type notification struct {
func (rcv *receiver) singleAlertNotifications(p *payload) []*notification { func (rcv *receiver) singleAlertNotifications(p *payload) []*notification {
var notifications []*notification var notifications []*notification
for _, alert := range p.Alerts { 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) n := new(notification)
// create title // 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() { func main() {
var configPath string var configPath string
flag.StringVar(&configPath, "config", "/etc/ntfy-alertmanager/config", "config file path") flag.StringVar(&configPath, "config", "/etc/ntfy-alertmanager/config", "config file path")
@ -298,7 +316,7 @@ func main() {
logger.Errorf("config: %v", err) 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) logger.Infof("Listening on %s, ntfy-alertmanager %s", cfg.HTTPAddress, version)
@ -309,5 +327,6 @@ func main() {
http.HandleFunc("/", receiver.handleWebhooks) http.HandleFunc("/", receiver.handleWebhooks)
} }
go receiver.runCleanup()
logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil)) logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil))
} }