2022-10-09 14:19:48 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-10-10 02:42:13 +02:00
|
|
|
"encoding/base64"
|
2022-10-09 19:50:46 +02:00
|
|
|
"encoding/json"
|
2022-10-10 01:20:18 +02:00
|
|
|
"flag"
|
2022-10-09 19:50:46 +02:00
|
|
|
"fmt"
|
2022-10-09 14:19:48 +02:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
2022-10-09 19:50:46 +02:00
|
|
|
"time"
|
2022-10-09 14:19:48 +02:00
|
|
|
|
|
|
|
"git.xenrox.net/~xenrox/go-log"
|
2022-10-09 19:50:46 +02:00
|
|
|
"golang.org/x/text/cases"
|
|
|
|
"golang.org/x/text/language"
|
2022-10-09 14:19:48 +02:00
|
|
|
)
|
|
|
|
|
2022-10-09 19:50:46 +02:00
|
|
|
type receiver struct {
|
2022-10-10 01:20:18 +02:00
|
|
|
cfg *config
|
2022-10-09 19:50:46 +02:00
|
|
|
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()
|
|
|
|
|
2022-10-09 20:03:55 +02:00
|
|
|
if r.Method != http.MethodPost {
|
2022-10-10 22:59:05 +02:00
|
|
|
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
|
2022-10-10 14:55:22 +02:00
|
|
|
rcv.logger.Debugf("illegal HTTP method: expected %q, got %q", "POST", r.Method)
|
2022-10-09 20:03:55 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
contentType := r.Header.Get("Content-Type")
|
|
|
|
if contentType != "application/json" {
|
2022-10-10 22:59:05 +02:00
|
|
|
http.Error(w, "Only application/json allowed", http.StatusUnsupportedMediaType)
|
2022-10-10 14:55:22 +02:00
|
|
|
rcv.logger.Debugf("illegal content type: %s", contentType)
|
2022-10-09 20:03:55 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-10-09 19:50:46 +02:00
|
|
|
var event payload
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
2022-10-10 14:55:22 +02:00
|
|
|
rcv.logger.Debug(err)
|
2022-10-09 19:50:46 +02:00
|
|
|
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}
|
2022-10-10 02:42:13 +02:00
|
|
|
req, err := http.NewRequest(http.MethodPost, rcv.cfg.ntfy.Topic, strings.NewReader(body))
|
2022-10-09 19:50:46 +02:00
|
|
|
if err != nil {
|
|
|
|
rcv.logger.Error(err)
|
|
|
|
}
|
|
|
|
|
2022-10-10 02:42:13 +02:00
|
|
|
// 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))
|
|
|
|
}
|
|
|
|
|
2022-10-09 19:50:46 +02:00
|
|
|
req.Header.Set("X-Title", title)
|
2022-10-12 16:35:04 +02:00
|
|
|
|
|
|
|
var priority string
|
2022-10-12 17:04:44 +02:00
|
|
|
var tags []string
|
2022-10-12 16:35:04 +02:00
|
|
|
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
|
|
|
|
}
|
2022-10-12 17:04:44 +02:00
|
|
|
|
|
|
|
for _, val := range labelConfig.Tags {
|
|
|
|
if !sliceContains(tags, val) {
|
|
|
|
tags = append(tags, val)
|
|
|
|
}
|
|
|
|
}
|
2022-10-12 16:35:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if priority != "" {
|
|
|
|
req.Header.Set("X-Priority", priority)
|
|
|
|
}
|
|
|
|
|
2022-10-12 17:04:44 +02:00
|
|
|
tagString := strings.Join(tags, ",")
|
|
|
|
if tagString != "" {
|
|
|
|
req.Header.Set("X-Tags", tagString)
|
|
|
|
}
|
|
|
|
|
2022-10-09 19:50:46 +02:00
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
rcv.logger.Error(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
2022-10-10 01:33:40 +02:00
|
|
|
rcv.logger.Errorf("ntfy: received status code %d", resp.StatusCode)
|
2022-10-10 14:55:22 +02:00
|
|
|
return
|
2022-10-09 19:50:46 +02:00
|
|
|
}
|
2022-10-09 14:19:48 +02:00
|
|
|
}
|
|
|
|
|
2022-10-10 19:55:33 +02:00
|
|
|
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 {
|
2022-10-10 22:59:05 +02:00
|
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
2022-10-10 19:55:33 +02:00
|
|
|
rcv.logger.Debug("basic auth: wrong user or password")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
handler(w, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-09 14:19:48 +02:00
|
|
|
func main() {
|
2022-10-10 01:20:18 +02:00
|
|
|
var configPath string
|
|
|
|
flag.StringVar(&configPath, "config", "/etc/ntfy-alertmanager/config", "config file path")
|
|
|
|
flag.Parse()
|
|
|
|
|
2022-10-09 14:19:48 +02:00
|
|
|
logger := log.NewDefaultLogger()
|
|
|
|
|
2022-10-10 01:20:18 +02:00
|
|
|
cfg, err := readConfig(configPath)
|
|
|
|
if err != nil {
|
|
|
|
logger.Fatalf("config: %v", err)
|
|
|
|
}
|
|
|
|
|
2022-10-13 01:15:04 +02:00
|
|
|
if err := logger.SetLevelFromString(cfg.LogLevel); err != nil {
|
2022-10-10 01:20:18 +02:00
|
|
|
logger.Errorf("config: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
receiver := &receiver{cfg: cfg, logger: logger}
|
2022-10-09 19:50:46 +02:00
|
|
|
|
2022-10-10 14:55:22 +02:00
|
|
|
logger.Infof("Listening on %s", cfg.HTTPAddress)
|
2022-10-10 19:55:33 +02:00
|
|
|
|
|
|
|
if cfg.User != "" && cfg.Password != "" {
|
|
|
|
logger.Info("Enabling HTTP Basic Authentication")
|
|
|
|
http.HandleFunc("/", receiver.basicAuthMiddleware(receiver.handleWebhooks))
|
|
|
|
} else {
|
|
|
|
http.HandleFunc("/", receiver.handleWebhooks)
|
|
|
|
}
|
|
|
|
|
2022-10-10 01:20:18 +02:00
|
|
|
logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil))
|
2022-10-09 14:19:48 +02:00
|
|
|
}
|