Use "log/slog" for logging

This commit is contained in:
Thorben Günther 2023-08-13 14:48:27 +02:00
parent 057b2e15f6
commit dab77f6393
No known key found for this signature in database
GPG key ID: 415CD778D8C5AFED
4 changed files with 75 additions and 39 deletions

4
go.mod
View file

@ -1,10 +1,10 @@
module git.xenrox.net/~xenrox/ntfy-alertmanager module git.xenrox.net/~xenrox/ntfy-alertmanager
go 1.21 go 1.21.0
require ( require (
git.sr.ht/~emersion/go-scfg v0.0.0-20230601130942-e042ab15616e 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 github.com/redis/go-redis/v9 v9.0.5
golang.org/x/text v0.12.0 golang.org/x/text v0.12.0
) )

4
go.sum
View file

@ -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 h1:42zyo0ZFxHGkysM1B9EM7PnQNO0TEzPm+bw/2Zontyg=
git.sr.ht/~emersion/go-scfg v0.0.0-20230601130942-e042ab15616e/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw= 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-utils v0.0.0-20230813014007-897165f99b2e h1:xqkh37YE78gxnJdlLrHc1WCRx3aYE/7XG8uZ6IJWCn0=
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/go.mod h1:BM4sMPD0fqFB6eG1T/7rGgEUiqZsMpHvq4PGE861Sfk=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 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/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=

72
main.go
View file

@ -10,6 +10,7 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -18,7 +19,7 @@ import (
"syscall" "syscall"
"time" "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/cache"
"git.xenrox.net/~xenrox/ntfy-alertmanager/config" "git.xenrox.net/~xenrox/ntfy-alertmanager/config"
"golang.org/x/text/cases" "golang.org/x/text/cases"
@ -29,7 +30,7 @@ var version = "dev"
type bridge struct { type bridge struct {
cfg *config.Config cfg *config.Config
logger *log.Logger logger *slog.Logger
cache cache.Cache cache cache.Cache
client *httpClient client *httpClient
} }
@ -72,10 +73,13 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification {
for _, alert := range p.Alerts { for _, alert := range p.Alerts {
contains, err := br.cache.Contains(alert.Fingerprint, alert.Status) contains, err := br.cache.Contains(alert.Fingerprint, alert.Status)
if err != nil { 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 { 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 continue
} }
@ -161,7 +165,8 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification {
s := &silenceBody{AlertManagerURL: p.ExternalURL, Labels: alert.Labels} s := &silenceBody{AlertManagerURL: p.ExternalURL, Labels: alert.Labels}
b, err := json.Marshal(s) b, err := json.Marshal(s)
if err != nil { 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) 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} s := &silenceBody{AlertManagerURL: p.ExternalURL, Labels: p.CommonLabels}
b, err := json.Marshal(s) b, err := json.Marshal(s)
if err != nil { 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) n.silenceBody = base64.StdEncoding.EncodeToString(b)
@ -332,7 +338,8 @@ func (br *bridge) publish(n *notification) error {
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
var ntfyError ntfyError var ntfyError ntfyError
if err := json.NewDecoder(resp.Body).Decode(&ntfyError); err != nil { 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) 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 { if r.Method != http.MethodPost {
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) 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 return
} }
contentType := r.Header.Get("Content-Type") contentType := r.Header.Get("Content-Type")
if contentType != "application/json" { if contentType != "application/json" {
http.Error(w, "Only application/json allowed", http.StatusUnsupportedMediaType) 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 return
} }
var event payload var event payload
if err := json.NewDecoder(r.Body).Decode(&event); err != nil { 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 return
} }
if br.logger.Level() == log.Debug { br.logger.Debug("Received alert",
br.logger.Debugf("Received alert %+v", event) slog.Any("payload", event))
}
if br.cfg.AlertMode == config.Single { if br.cfg.AlertMode == config.Single {
notifications := br.singleAlertNotifications(&event) notifications := br.singleAlertNotifications(&event)
for _, n := range notifications { for _, n := range notifications {
err := br.publish(n) err := br.publish(n)
if err != nil { 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 { } else {
if err := br.cache.Set(n.fingerprint, n.status); err != nil { 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) notification := br.multiAlertNotification(&event)
err := br.publish(notification) err := br.publish(notification)
if err != nil { 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) 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) cfg, err := config.ReadConfig(configPath)
if err != nil { 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 { level, err := logging.ParseLevelFromString(cfg.LogLevel)
logger.Errorf("Failed to parse logging level: %v", err) 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) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop() defer stop()
@ -457,11 +476,13 @@ func main() {
c, err := cache.NewCache(cfg.Cache) c, err := cache.NewCache(cfg.Cache)
if err != nil { 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} 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 := http.NewServeMux()
mux.HandleFunc("/", bridge.handleWebhooks) mux.HandleFunc("/", bridge.handleWebhooks)
@ -483,7 +504,9 @@ func main() {
go func() { go func() {
err = httpServer.ListenAndServe() err = httpServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed { 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) err = httpServer.Shutdown(httpShutdownContext)
if err != nil { if err != nil {
logger.Errorf("Failed to shutdown HTTP server: %v", err) logger.Error("Failed to shutdown HTTP server",
slog.String("error", err.Error()))
} }
} }

View file

@ -4,7 +4,9 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"time" "time"
) )
@ -40,26 +42,29 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) 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 return
} }
b, err := io.ReadAll(r.Body) b, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
br.logger.Debugf("silences: %v", err) br.logger.Error("Silences: Failed to read body",
slog.String("error", err.Error()))
return return
} }
b, err = base64.StdEncoding.DecodeString(string(b)) b, err = base64.StdEncoding.DecodeString(string(b))
if err != nil { if err != nil {
br.logger.Debugf("silences: %v", err) br.logger.Error("Silences: Failed to decode",
slog.String("error", err.Error()))
return return
} }
var sb silenceBody var sb silenceBody
err = json.Unmarshal(b, &sb) err = json.Unmarshal(b, &sb)
if err != nil { if err != nil {
br.logger.Debugf("silences: %v", err) br.logger.Error("Silences: Failed to unmarshal",
slog.String("error", err.Error()))
return return
} }
@ -85,7 +90,8 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
b, err = json.Marshal(silence) b, err = json.Marshal(silence)
if err != nil { if err != nil {
br.logger.Debugf("silences: %v", err) br.logger.Error("Silences: Failed to marshal",
slog.String("error", err.Error()))
return 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)) req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(b))
if err != nil { if err != nil {
br.logger.Debugf("silences: %v", err) br.logger.Error("Silences: Failed to create request",
slog.String("error", err.Error()))
return return
} }
@ -109,27 +116,32 @@ func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
resp, err := br.client.Do(req) resp, err := br.client.Do(req)
if err != nil { if err != nil {
br.logger.Debugf("silences: %v", err) br.logger.Error("Silences: Failed to POST request",
slog.String("error", err.Error()))
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
b, err = io.ReadAll(resp.Body) b, err = io.ReadAll(resp.Body)
if err != nil { if err != nil {
br.logger.Debugf("silences: %v", err) br.logger.Error("Silences: Failed to read response body",
slog.String("error", err.Error()))
return return
} }
if resp.StatusCode != http.StatusOK { 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 return
} }
var id silenceResponse var id silenceResponse
if err := json.Unmarshal(b, &id); err != nil { 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 return
} }
br.logger.Infof("Created new silence %s", id.ID) br.logger.Info("Silences: Created new silence",
slog.String("ID", id.ID))
} }