diff --git a/go.mod b/go.mod index 0483613..9e93fec 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module git.xenrox.net/~xenrox/ntfy-alertmanager -go 1.21 +go 1.21.0 require ( git.sr.ht/~emersion/go-scfg v0.0.0-20230601130942-e042ab15616e - git.xenrox.net/~xenrox/go-log v0.0.0-20221012231312-9e7356c29b74 + git.xenrox.net/~xenrox/go-utils v0.0.0-20230813014007-897165f99b2e github.com/redis/go-redis/v9 v9.0.5 golang.org/x/text v0.12.0 ) diff --git a/go.sum b/go.sum index bee8e4c..2ab0d72 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ git.sr.ht/~emersion/go-scfg v0.0.0-20230601130942-e042ab15616e h1:42zyo0ZFxHGkysM1B9EM7PnQNO0TEzPm+bw/2Zontyg= git.sr.ht/~emersion/go-scfg v0.0.0-20230601130942-e042ab15616e/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw= -git.xenrox.net/~xenrox/go-log v0.0.0-20221012231312-9e7356c29b74 h1:t/52xLRU4IHd2O1nkb9fcUE6K95/KdBtdQYHT31szps= -git.xenrox.net/~xenrox/go-log v0.0.0-20221012231312-9e7356c29b74/go.mod h1:d98WFDHGpxaEThKue5CfGtr9OrWgbaApprt3GH+OM4s= +git.xenrox.net/~xenrox/go-utils v0.0.0-20230813014007-897165f99b2e h1:xqkh37YE78gxnJdlLrHc1WCRx3aYE/7XG8uZ6IJWCn0= +git.xenrox.net/~xenrox/go-utils v0.0.0-20230813014007-897165f99b2e/go.mod h1:BM4sMPD0fqFB6eG1T/7rGgEUiqZsMpHvq4PGE861Sfk= github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= diff --git a/main.go b/main.go index 6ef1541..cbde0bb 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "encoding/json" "flag" "fmt" + "log/slog" "net/http" "os" "os/signal" @@ -18,7 +19,7 @@ import ( "syscall" "time" - "git.xenrox.net/~xenrox/go-log" + "git.xenrox.net/~xenrox/go-utils/logging" "git.xenrox.net/~xenrox/ntfy-alertmanager/cache" "git.xenrox.net/~xenrox/ntfy-alertmanager/config" "golang.org/x/text/cases" @@ -29,7 +30,7 @@ var version = "dev" type bridge struct { cfg *config.Config - logger *log.Logger + logger *slog.Logger cache cache.Cache client *httpClient } @@ -72,10 +73,13 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification { for _, alert := range p.Alerts { contains, err := br.cache.Contains(alert.Fingerprint, alert.Status) if err != nil { - br.logger.Errorf("Failed to lookup alert %q in cache: %v", alert.Fingerprint, err) + br.logger.Error("Failed to lookup alert in cache", + slog.String("fingerprint", alert.Fingerprint), + slog.String("error", err.Error())) } if contains { - br.logger.Debugf("Alert %q skipped: Still in cache", alert.Fingerprint) + br.logger.Debug("Alert skipped: Still in cache", + slog.String("fingerprint", alert.Fingerprint)) continue } @@ -161,7 +165,8 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification { s := &silenceBody{AlertManagerURL: p.ExternalURL, Labels: alert.Labels} b, err := json.Marshal(s) if err != nil { - br.logger.Errorf("Failed to create silence action: %v", err) + br.logger.Error("Failed to create silence action", + slog.String("error", err.Error())) } n.silenceBody = base64.StdEncoding.EncodeToString(b) @@ -266,7 +271,8 @@ func (br *bridge) multiAlertNotification(p *payload) *notification { s := &silenceBody{AlertManagerURL: p.ExternalURL, Labels: p.CommonLabels} b, err := json.Marshal(s) if err != nil { - br.logger.Errorf("Failed to create silence action: %v", err) + br.logger.Error("Failed to create silence action", + slog.String("error", err.Error())) } n.silenceBody = base64.StdEncoding.EncodeToString(b) @@ -332,7 +338,8 @@ func (br *bridge) publish(n *notification) error { if resp.StatusCode != http.StatusOK { var ntfyError ntfyError if err := json.NewDecoder(resp.Body).Decode(&ntfyError); err != nil { - br.logger.Debugf("Publish: failed to decode error: %v", err) + br.logger.Debug("Publish: Failed to decode error", + slog.String("error", err.Error())) return fmt.Errorf("ntfy: received status code %d", resp.StatusCode) } @@ -347,36 +354,39 @@ func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) - br.logger.Debugf("illegal HTTP method: expected %q, got %q", "POST", r.Method) + br.logger.Debug(fmt.Sprintf("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) - br.logger.Debugf("illegal content type: %s", contentType) + br.logger.Debug(fmt.Sprintf("Illegal content type: %s", contentType)) return } var event payload if err := json.NewDecoder(r.Body).Decode(&event); err != nil { - br.logger.Debug(err) + br.logger.Debug("Failed to decode payload", + slog.String("error", err.Error())) return } - if br.logger.Level() == log.Debug { - br.logger.Debugf("Received alert %+v", event) - } + br.logger.Debug("Received alert", + slog.Any("payload", event)) if br.cfg.AlertMode == config.Single { notifications := br.singleAlertNotifications(&event) for _, n := range notifications { err := br.publish(n) if err != nil { - br.logger.Errorf("Failed to publish notification: %v", err) + br.logger.Error("Failed to publish notification", + slog.String("error", err.Error())) } else { if err := br.cache.Set(n.fingerprint, n.status); err != nil { - br.logger.Errorf("Failed to set alert %q in cache: %v", n.fingerprint, err) + br.logger.Error("Failed to cache alert", + slog.String("fingerprint", n.fingerprint), + slog.String("error", err.Error())) } } } @@ -384,7 +394,8 @@ func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) { notification := br.multiAlertNotification(&event) err := br.publish(notification) if err != nil { - br.logger.Errorf("Failed to publish notification: %v", err) + br.logger.Error("Failed to publish notification", + slog.String("error", err.Error())) } } } @@ -439,16 +450,24 @@ func main() { os.Exit(0) } - logger := log.NewDefaultLogger() + logLevel := new(slog.LevelVar) + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: logLevel, + })) cfg, err := config.ReadConfig(configPath) if err != nil { - logger.Fatalf("Failed to read config: %v", err) + logger.Error("Failed to read config", + slog.String("error", err.Error())) + os.Exit(1) } - if err := logger.SetLevelFromString(cfg.LogLevel); err != nil { - logger.Errorf("Failed to parse logging level: %v", err) + level, err := logging.ParseLevelFromString(cfg.LogLevel) + if err != nil { + logger.Error("Failed to parse logging level", + slog.String("error", err.Error())) } + logLevel.Set(level) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() @@ -457,11 +476,13 @@ func main() { c, err := cache.NewCache(cfg.Cache) if err != nil { - logger.Fatalf("Failed to create cache: %v", err) + logger.Error("Failed to create cache", + slog.String("error", err.Error())) + os.Exit(1) } bridge := &bridge{cfg: cfg, logger: logger, cache: c, client: client} - logger.Infof("Listening on %s, ntfy-alertmanager %s", cfg.HTTPAddress, version) + logger.Info(fmt.Sprintf("Listening on %s, ntfy-alertmanager %s", cfg.HTTPAddress, version)) mux := http.NewServeMux() mux.HandleFunc("/", bridge.handleWebhooks) @@ -483,7 +504,9 @@ func main() { go func() { err = httpServer.ListenAndServe() if err != nil && err != http.ErrServerClosed { - logger.Fatalf("Failed to start HTTP server: %v", err) + logger.Error("Failed to start HTTP server", + slog.String("error", err.Error())) + os.Exit(1) } }() @@ -495,6 +518,7 @@ func main() { err = httpServer.Shutdown(httpShutdownContext) if err != nil { - logger.Errorf("Failed to shutdown HTTP server: %v", err) + logger.Error("Failed to shutdown HTTP server", + slog.String("error", err.Error())) } } diff --git a/silence.go b/silence.go index d372c29..2304f1c 100644 --- a/silence.go +++ b/silence.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/base64" "encoding/json" + "fmt" "io" + "log/slog" "net/http" "time" ) @@ -40,26 +42,29 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) - br.logger.Debugf("silences: illegal HTTP method: expected %q, got %q", "POST", r.Method) + br.logger.Debug(fmt.Sprintf("Silences: Illegal HTTP method: expected %q, got %q", "POST", r.Method)) return } b, err := io.ReadAll(r.Body) if err != nil { - br.logger.Debugf("silences: %v", err) + br.logger.Error("Silences: Failed to read body", + slog.String("error", err.Error())) return } b, err = base64.StdEncoding.DecodeString(string(b)) if err != nil { - br.logger.Debugf("silences: %v", err) + br.logger.Error("Silences: Failed to decode", + slog.String("error", err.Error())) return } var sb silenceBody err = json.Unmarshal(b, &sb) if err != nil { - br.logger.Debugf("silences: %v", err) + br.logger.Error("Silences: Failed to unmarshal", + slog.String("error", err.Error())) return } @@ -85,7 +90,8 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) { b, err = json.Marshal(silence) if err != nil { - br.logger.Debugf("silences: %v", err) + br.logger.Error("Silences: Failed to marshal", + slog.String("error", err.Error())) return } @@ -97,7 +103,8 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) { req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(b)) if err != nil { - br.logger.Debugf("silences: %v", err) + br.logger.Error("Silences: Failed to create request", + slog.String("error", err.Error())) return } @@ -109,27 +116,32 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) { req.Header.Add("Content-Type", "application/json") resp, err := br.client.Do(req) if err != nil { - br.logger.Debugf("silences: %v", err) + br.logger.Error("Silences: Failed to POST request", + slog.String("error", err.Error())) return } defer resp.Body.Close() b, err = io.ReadAll(resp.Body) if err != nil { - br.logger.Debugf("silences: %v", err) + br.logger.Error("Silences: Failed to read response body", + slog.String("error", err.Error())) return } if resp.StatusCode != http.StatusOK { - br.logger.Debugf("silences: received status code %d", resp.StatusCode) + br.logger.Error("Silences: Received non-200 status code", + slog.Int("status", resp.StatusCode)) return } var id silenceResponse if err := json.Unmarshal(b, &id); err != nil { - br.logger.Debugf("silences: %v", err) + br.logger.Error("Silences: Failed to unmarshal response", + slog.String("error", err.Error())) return } - br.logger.Infof("Created new silence %s", id.ID) + br.logger.Info("Silences: Created new silence", + slog.String("ID", id.ID)) }