ntfy-alertmanager/main.go
Thorben Günther 9043ccfb5e
Set ntfy priority depending on Prometheus labels
It is possible to configure a priority for an arbitrary label.
Furthermore a priority/order of those arbitrary labels should be
defined. The Alertmanager payload is checked for these and the
priority will be selected from the highest valued label that has one
set in the configuration file.
2022-10-12 16:35:04 +02:00

181 lines
4.4 KiB
Go

package main
import (
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"net/http"
"strings"
"time"
"git.xenrox.net/~xenrox/go-log"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type receiver struct {
cfg *config
logger *log.Logger
}
type payload struct {
Status string `json:"status"`
Alerts []alert `json:"alerts"`
GroupLabels map[string]interface{} `json:"groupLabels"`
CommonLabels map[string]interface{} `json:"commonLabels"`
CommonAnnotations map[string]interface{} `json:"commonAnnotations"`
}
type alert struct {
Status string `json:"status"`
Labels map[string]interface{} `json:"labels"`
Annotations map[string]interface{} `json:"annotations"`
}
func (rcv *receiver) 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)
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)
return
}
var event payload
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
rcv.logger.Debug(err)
return
}
count := len(event.Alerts)
title := fmt.Sprintf("[%s", strings.ToUpper(event.Status))
if event.Status == "firing" {
title = fmt.Sprintf("%s:%d", title, count)
}
title += "]"
for _, value := range event.GroupLabels {
title = fmt.Sprintf("%s %s", title, value)
}
var body string
c := cases.Title(language.English)
for _, alert := range event.Alerts {
alertBody := fmt.Sprintf("%s\nLabels:\n", c.String(alert.Status))
for key, value := range alert.Labels {
alertBody = fmt.Sprintf("%s%s = %s\n", alertBody, key, value)
}
alertBody += "Annotations:\n"
for key, value := range alert.Annotations {
alertBody = fmt.Sprintf("%s%s = %s\n", alertBody, key, value)
}
alertBody += "\n"
body += alertBody
}
client := &http.Client{Timeout: time.Second * 3}
req, err := http.NewRequest(http.MethodPost, rcv.cfg.ntfy.Topic, strings.NewReader(body))
if err != nil {
rcv.logger.Error(err)
}
// Basic auth
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)))
req.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth))
}
req.Header.Set("X-Title", title)
var priority string
for _, labelName := range rcv.cfg.labels.Order {
val, ok := event.CommonLabels[labelName]
if !ok {
continue
}
labelConfig, ok := rcv.cfg.labels.Label[fmt.Sprintf("%s:%s", labelName, val)]
if !ok {
continue
}
if priority == "" {
priority = labelConfig.Priority
}
}
if priority != "" {
req.Header.Set("X-Priority", priority)
}
resp, err := client.Do(req)
if err != nil {
rcv.logger.Error(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
rcv.logger.Errorf("ntfy: received status code %d", resp.StatusCode)
return
}
}
func (rcv *receiver) 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")
return
}
if user != rcv.cfg.User || pass != rcv.cfg.Password {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
rcv.logger.Debug("basic auth: wrong user or password")
return
}
handler(w, r)
}
}
func main() {
var configPath string
flag.StringVar(&configPath, "config", "/etc/ntfy-alertmanager/config", "config file path")
flag.Parse()
logger := log.NewDefaultLogger()
cfg, err := readConfig(configPath)
if err != nil {
logger.Fatalf("config: %v", err)
}
if err := logger.ParseLevel(cfg.LogLevel); err != nil {
logger.Errorf("config: %v", err)
}
receiver := &receiver{cfg: cfg, logger: logger}
logger.Infof("Listening on %s", cfg.HTTPAddress)
if cfg.User != "" && cfg.Password != "" {
logger.Info("Enabling HTTP Basic Authentication")
http.HandleFunc("/", receiver.basicAuthMiddleware(receiver.handleWebhooks))
} else {
http.HandleFunc("/", receiver.handleWebhooks)
}
logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil))
}