diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8cc730b..c3e1527 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.21 + go-version: 1.22 - name: Login to GitHub Container Registry uses: docker/login-action@v3 diff --git a/.gitignore b/.gitignore index 256a8bb..800228a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ bin/ data/ dist/ *.idea/ +.cache/ diff --git a/Makefile b/Makefile index b82be80..9bbaa8c 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ build: ## Build binary. .PHONY: run run: ## Run binary. - ./${APP-BIN} + ./${APP-BIN} --config ./config.toml .PHONY: clean clean: ## Remove temporary files and the `bin` folder. @@ -27,10 +27,10 @@ lint: .PHONY: dev-docker dev-docker: clean build ## Build and spawns docker containers for the entire suite (Alertmanager/Prometheus/calert). cd dev; \ - docker-compose build ; \ - CURRENT_UID=$(id -u):$(id -g) docker-compose up + echo "Current UID: $(shell id -u):$(shell id -g)"; \ + CURRENT_UID=$(shell id -u):$(shell id -g) docker compose up .PHONY: rm-dev-docker rm-dev-docker: clean build ## Delete the docker containers including volumes. cd dev; \ - docker-compose down -v ; \ + docker compose down -v ; \ diff --git a/cmd/handlers.go b/cmd/handlers.go index f598740..3ddd9f9 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -88,7 +88,7 @@ func handleDispatchNotif(w http.ResponseWriter, r *http.Request) { // Unmarshall POST Body. if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - app.lo.WithError(err).Error("error decoding request body") + app.lo.Error("error decoding request body", "error", err) app.metrics.Increment(`http_request_errors_total{handler="dispatch"}`) sendErrorResponse(w, "Error decoding payload.", http.StatusBadRequest, nil) return @@ -99,14 +99,14 @@ func handleDispatchNotif(w http.ResponseWriter, r *http.Request) { roomName = payload.Receiver } - app.lo.WithField("receiver", roomName).Info("dispatching new alert") + app.lo.Info("dispatching new alert", "room", roomName, "count", len(payload.Alerts)) // Dispatch a list of alerts via Notifier. // If there are a lot of alerts (>=10) to push, G-Chat API can be extremely slow to add messages // to an existing thread. So it's better to enqueue it in background. go func() { if err := app.notifier.Dispatch(payload.Alerts, roomName); err != nil { - app.lo.WithError(err).Error("error dispatching alerts") + app.lo.Error("error dispatching alerts", "error", err) app.metrics.Increment(`http_request_errors_total{handler="dispatch"}`) } app.metrics.Duration(`http_request_duration_seconds{handler="dispatch"}`, now) diff --git a/cmd/init.go b/cmd/init.go index 457acd9..ce6d6a5 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "log" + "log/slog" "os" "strings" @@ -13,24 +15,24 @@ import ( "github.com/mr-karan/calert/internal/notifier" prvs "github.com/mr-karan/calert/internal/providers" "github.com/mr-karan/calert/internal/providers/google_chat" - "github.com/sirupsen/logrus" flag "github.com/spf13/pflag" ) // initLogger initializes logger instance. -func initLogger() *logrus.Logger { - logger := logrus.New() - - logger.SetFormatter(&logrus.TextFormatter{ - FullTimestamp: true, - DisableLevelTruncation: true, - }) +func initLogger(verbose bool) *slog.Logger { + lvl := slog.LevelInfo + if verbose { + lvl = slog.LevelDebug + } - return logger + return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: lvl, + AddSource: true, + })) } // initConfig loads config to `ko` object. -func initConfig(lo *logrus.Logger, cfgDefault string, envPrefix string) (*koanf.Koanf, error) { +func initConfig(cfgDefault string, envPrefix string) (*koanf.Koanf, error) { var ( ko = koanf.New(".") f = flag.NewFlagSet("front", flag.ContinueOnError) @@ -52,19 +54,19 @@ func initConfig(lo *logrus.Logger, cfgDefault string, envPrefix string) (*koanf. } // Load the config files from the path provided. - lo.WithField("path", *cfgPath).Info("attempting to load config from file") + log.Printf("attempting to load config from file: %s\n", *cfgPath) err = ko.Load(file.Provider(*cfgPath), toml.Parser()) if err != nil { // If the default config is not present, print a warning and continue reading the values from env. if *cfgPath == cfgDefault { - lo.WithError(err).Warn("unable to open sample config file") + log.Printf("unable to open config file: %w falling back to env vars\n", err.Error()) } else { return nil, err } } - lo.Info("attempting to read config from env vars") + log.Println("attempting to read config from env vars") // Load environment variables if the key is given // and merge into the loaded config. if envPrefix != "" { @@ -81,7 +83,7 @@ func initConfig(lo *logrus.Logger, cfgDefault string, envPrefix string) (*koanf. } // initProviders loads all the providers specified in the config. -func initProviders(ko *koanf.Koanf, lo *logrus.Logger, metrics *metrics.Manager) []prvs.Provider { +func initProviders(ko *koanf.Koanf, lo *slog.Logger, metrics *metrics.Manager) ([]prvs.Provider, error) { provs := make([]prvs.Provider, 0) // Loop over all providers listed in config. @@ -106,32 +108,32 @@ func initProviders(ko *koanf.Koanf, lo *logrus.Logger, metrics *metrics.Manager) }, ) if err != nil { - lo.WithError(err).Fatal("error initialising google chat provider") + return nil, fmt.Errorf("error initialising google chat provider: %s", err) } - lo.WithField("room", gchat.Room()).Info("initialised provider") + lo.Info("initialised provider", "room", gchat.Room()) provs = append(provs, gchat) } } if len(provs) == 0 { - lo.Fatal("no providers listed in config") + return nil, fmt.Errorf("no providers listed in config") } - return provs + return provs, nil } // initNotifier initializes a Notifier instance. -func initNotifier(ko *koanf.Koanf, lo *logrus.Logger, provs []prvs.Provider) notifier.Notifier { +func initNotifier(ko *koanf.Koanf, lo *slog.Logger, provs []prvs.Provider) (notifier.Notifier, error) { n, err := notifier.Init(notifier.Opts{ Providers: provs, Log: lo, }) if err != nil { - lo.WithError(err).Fatal("error initialising notifier") + return notifier.Notifier{}, fmt.Errorf("error initialising notifier: %s", err) } - return n + return n, err } // initMetrics initializes a Metrics manager. diff --git a/cmd/main.go b/cmd/main.go index 9f83433..76c40ff 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,13 +2,14 @@ package main import ( "net/http" + "os" - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/mr-karan/calert/internal/metrics" "github.com/mr-karan/calert/internal/notifier" - "github.com/sirupsen/logrus" + "log/slog" ) var ( @@ -19,31 +20,41 @@ var ( // App is the global contains // instances of various objects used in the lifecyle of program. type App struct { - lo *logrus.Logger + lo *slog.Logger metrics *metrics.Manager notifier notifier.Notifier } func main() { - // Initialise logger. - lo := initLogger() - // Initialise and load the config. - ko, err := initConfig(lo, "config.sample.toml", "CALERT_") + ko, err := initConfig("config.sample.toml", "CALERT_") if err != nil { - // Need to `panic` since logger can only be initialised once config is initialised. panic(err.Error()) } var ( - metrics = initMetrics() - provs = initProviders(ko, lo, metrics) - notifier = initNotifier(ko, lo, provs) + metrics = initMetrics() ) - // Enable debug mode if specified. + // Initialise logger. + verbose := false if ko.String("app.log") == "debug" { - lo.SetLevel(logrus.DebugLevel) + verbose = true + } + lo := initLogger(verbose) + + // Initialise providers. + provs, err := initProviders(ko, lo, metrics) + if err != nil { + lo.Error("error initialising providers", "error", err) + exit() + } + + // Initialise notifier. + notifier, err := initNotifier(ko, lo, provs) + if err != nil { + lo.Error("error initialising notifier", "error", err) + exit() } app := &App{ @@ -52,7 +63,7 @@ func main() { metrics: metrics, } - app.lo.WithField("version", buildString).Info("booting calert") + app.lo.Info("starting calert", "version", buildString, "verbose", verbose) // Initialise HTTP Router. r := chi.NewRouter() @@ -70,7 +81,7 @@ func main() { r.Post("/dispatch", wrap(app, handleDispatchNotif)) // Start HTTP Server. - app.lo.WithField("addr", ko.MustString("app.address")).Info("starting http server") + app.lo.Info("starting http server", "address", ko.MustString("app.address")) srv := &http.Server{ Addr: ko.MustString("app.address"), ReadTimeout: ko.MustDuration("app.server_timeout"), @@ -78,6 +89,11 @@ func main() { Handler: r, } if err := srv.ListenAndServe(); err != nil { - app.lo.WithError(err).Fatal("couldn't start server") + app.lo.Error("couldn't start server", "error", err) + exit() } } + +func exit() { + os.Exit(1) +} diff --git a/dev/Dockerfile.dev b/dev/Dockerfile.dev index 52b5022..4a08ad8 100644 --- a/dev/Dockerfile.dev +++ b/dev/Dockerfile.dev @@ -1,4 +1,15 @@ -FROM golang:1.18 as builder -# Disable CGO +FROM golang:1.22 as builder + ENV CGO_ENABLED=0 WORKDIR /app + +# Prepare cache directories and ensure correct permissions +RUN mkdir -p /app/.cache/go-build /app/.cache/go-mod && \ + chown -R 1000:1000 /app/.cache + +# Switch to non-root user +USER 1000:1000 + +# Set environment variables for caches +ENV GOCACHE=/app/.cache/go-build +ENV GOMODCACHE=/app/.cache/go-mod diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index dd6468f..f3036e3 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -3,7 +3,7 @@ networks: driver: bridge volumes: - prometheus_data: {} + prometheus_data: {} services: @@ -11,18 +11,20 @@ services: build: context: ../ dockerfile: dev/Dockerfile.dev - command: ["make", "fresh"] + command: [ "make", "fresh" ] ports: - "6000:6000" volumes: - ../:/app - - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache networks: - monitor-net user: ${CURRENT_UID} + environment: + - GOCACHE=/app/.cache/go-build + - GOMODCACHE=/app/.cache/go-mod alertmanager: - image: prom/alertmanager:v0.23.0 + image: prom/alertmanager:v0.26.0 container_name: alertmanager volumes: - ./alertmanager:/etc/alertmanager @@ -38,7 +40,7 @@ services: - calert prometheus: - image: prom/prometheus:v2.32.1 + image: prom/prometheus:v2.50.0 container_name: prometheus volumes: - ./prometheus:/etc/prometheus diff --git a/go.mod b/go.mod index d081709..4e7daab 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/mr-karan/calert require ( - github.com/VictoriaMetrics/metrics v1.24.0 - github.com/go-chi/chi v1.5.5 + github.com/VictoriaMetrics/metrics v1.32.0 + github.com/go-chi/chi/v5 v5.0.12 github.com/gofrs/uuid v4.4.0+incompatible github.com/knadh/koanf v1.5.0 github.com/prometheus/alertmanager v0.26.0 - github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 + golang.org/x/text v0.14.0 ) require ( @@ -16,24 +16,22 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.17.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.47.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/histogram v1.2.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + golang.org/x/sys v0.17.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c1466b5..a228cf5 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/VictoriaMetrics/metrics v1.24.0 h1:ILavebReOjYctAGY5QU2F9X0MYvkcrG3aEn2RKa1Zkw= -github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys= +github.com/VictoriaMetrics/metrics v1.32.0 h1:r9JK2zndYv0TIxFXLEHwhQqRdnu8/O3cwJiCBX4vJCM= +github.com/VictoriaMetrics/metrics v1.32.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -53,8 +53,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= -github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -96,8 +96,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -171,8 +171,6 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -219,19 +217,19 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= -github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k= +github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= @@ -253,8 +251,6 @@ github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92/go.mod h1:7/OT02F6 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -352,9 +348,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -409,8 +404,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/notifier/notifier.go b/internal/notifier/notifier.go index 20ab05b..49a0a79 100644 --- a/internal/notifier/notifier.go +++ b/internal/notifier/notifier.go @@ -2,22 +2,22 @@ package notifier import ( "fmt" + "log/slog" "github.com/mr-karan/calert/internal/providers" alertmgrtmpl "github.com/prometheus/alertmanager/template" - "github.com/sirupsen/logrus" ) // Notifier represents an instance that pushes out notifications to // upstream providers. type Notifier struct { providers map[string]providers.Provider - lo *logrus.Logger + lo *slog.Logger } type Opts struct { Providers []providers.Provider - Log *logrus.Logger + Log *slog.Logger } // Init initialises a new instance of the Notifier. @@ -38,11 +38,11 @@ func Init(opts Opts) (Notifier, error) { // Dispatch pushes out a notification to an upstream provider. func (n *Notifier) Dispatch(alerts []alertmgrtmpl.Alert, room string) error { - n.lo.WithField("count", len(alerts)).Info("dispatching alerts") + n.lo.Info("dispatching alerts", "count", len(alerts)) // Lookup for the provider by the room name. if _, ok := n.providers[room]; !ok { - n.lo.WithField("room", room).Warn("no provider available for room") + n.lo.Error("no provider available for room", "room", room) return fmt.Errorf("no provider configured for room: %s", room) } // Push the batch of alerts. diff --git a/internal/providers/google_chat/alerts.go b/internal/providers/google_chat/alerts.go index 757a4d3..814022f 100644 --- a/internal/providers/google_chat/alerts.go +++ b/internal/providers/google_chat/alerts.go @@ -1,19 +1,19 @@ package google_chat import ( + "log/slog" "sync" "time" "github.com/gofrs/uuid" "github.com/mr-karan/calert/internal/metrics" alertmgrtmpl "github.com/prometheus/alertmanager/template" - "github.com/sirupsen/logrus" ) // ActiveAlerts represents a map of alerts unique fingerprint hash // with their details. type ActiveAlerts struct { - lo *logrus.Logger + lo *slog.Logger metrics *metrics.Manager sync.RWMutex alerts map[string]AlertDetails @@ -82,7 +82,7 @@ func (d *ActiveAlerts) Prune(ttl time.Duration) { for k, a := range d.alerts { // If the alert creation field is past our specified TTL, remove it from the map. if a.StartsAt.Before(expired) { - d.lo.WithField("fingerprint", k).WithField("created", a.StartsAt).WithField("expired", expired).Debug("removing alert from active alerts") + d.lo.Debug("removing alert from active alerts", "fingerprint", k, "created", a.StartsAt, "expired", expired) delete(d.alerts, k) } } diff --git a/internal/providers/google_chat/google_chat.go b/internal/providers/google_chat/google_chat.go index b9d723b..11faa31 100644 --- a/internal/providers/google_chat/google_chat.go +++ b/internal/providers/google_chat/google_chat.go @@ -2,6 +2,7 @@ package google_chat import ( "fmt" + "log/slog" "net/http" "net/url" "path/filepath" @@ -11,11 +12,12 @@ import ( "github.com/mr-karan/calert/internal/metrics" alertmgrtmpl "github.com/prometheus/alertmanager/template" - "github.com/sirupsen/logrus" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) type GoogleChatManager struct { - lo *logrus.Logger + lo *slog.Logger metrics *metrics.Manager activeAlerts *ActiveAlerts endpoint string @@ -26,7 +28,7 @@ type GoogleChatManager struct { } type GoogleChatOpts struct { - Log *logrus.Logger + Log *slog.Logger Metrics *metrics.Manager DryRun bool MaxIdleConn int @@ -64,7 +66,11 @@ func NewGoogleChat(opts GoogleChatOpts) (*GoogleChatManager, error) { // Initialise message template functions. templateFuncMap := template.FuncMap{ - "Title": strings.Title, + "Title": func(s string) string { + // Create a Title cased string respecting Unicode rules. + titleCaser := cases.Title(language.English) + return titleCaser.String(s) + }, "toUpper": strings.ToUpper, "Contains": strings.Contains, } @@ -97,7 +103,7 @@ func NewGoogleChat(opts GoogleChatOpts) (*GoogleChatManager, error) { // Push accepts the list of alerts and dispatches them to Webhook API endpoint. func (m *GoogleChatManager) Push(alerts []alertmgrtmpl.Alert) error { - m.lo.WithField("count", len(alerts)).Info("dispatching alerts to google chat") + m.lo.Info("dispatching alerts to google chat", "count", len(alerts)) // For each alert, lookup the UUID and send the alert. for _, a := range alerts { @@ -109,7 +115,7 @@ func (m *GoogleChatManager) Push(alerts []alertmgrtmpl.Alert) error { // Prepare a list of messages to send. msgs, err := m.prepareMessage(a) if err != nil { - m.lo.WithError(err).Error("error preparing message") + m.lo.Error("error preparing message", "error", err) continue } @@ -124,11 +130,11 @@ func (m *GoogleChatManager) Push(alerts []alertmgrtmpl.Alert) error { // Send message to API. if m.dryRun { - m.lo.WithField("room", m.Room()).Info("dry_run is enabled for this room. skipping pushing notification") + m.lo.Info("dry_run is enabled for this room. skipping pushing notification", "room", m.Room()) } else { if err := m.sendMessage(msg, threadKey); err != nil { m.metrics.Increment(fmt.Sprintf(`alerts_dispatched_errors_total{provider="%s", room="%s"}`, m.ID(), m.Room())) - m.lo.WithError(err).Error("error sending message") + m.lo.Error("error sending message", "error", err) continue } } diff --git a/internal/providers/google_chat/google_chat_test.go b/internal/providers/google_chat/google_chat_test.go index 8a7f2bd..c934597 100644 --- a/internal/providers/google_chat/google_chat_test.go +++ b/internal/providers/google_chat/google_chat_test.go @@ -1,18 +1,20 @@ package google_chat import ( + "os" "path/filepath" "testing" + "log/slog" + alertmgrtmpl "github.com/prometheus/alertmanager/template" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestGoogleChatTemplate(t *testing.T) { opts := &GoogleChatOpts{ - Log: logrus.New(), + Log: slog.New(slog.NewJSONHandler(os.Stdout, nil)), Endpoint: "http://", Room: "qa", Template: "../../../static/message.tmpl", diff --git a/internal/providers/google_chat/message.go b/internal/providers/google_chat/message.go index c9e784f..20511ac 100644 --- a/internal/providers/google_chat/message.go +++ b/internal/providers/google_chat/message.go @@ -3,7 +3,8 @@ package google_chat import ( "bytes" "encoding/json" - "errors" + "fmt" + "io" "net/http" "net/url" "strings" @@ -30,7 +31,7 @@ func (m *GoogleChatManager) prepareMessage(alert alertmgrtmpl.Alert) ([]ChatMess // Render a template with alert data. err := m.msgTmpl.Execute(&to, alert) if err != nil { - m.lo.WithError(err).Error("Error parsing values in template") + m.lo.Error("Error parsing values in template", "error", err) return messages, err } @@ -78,7 +79,7 @@ func (m *GoogleChatManager) sendMessage(msg ChatMessage, threadKey string) error req.Header.Set("Content-Type", "application/json") // Send the request. - m.lo.WithField("url", endpoint).WithField("msg", msg.Text).Debug("sending alert") + m.lo.Debug("sending alert", "url", endpoint, "msg", msg.Text) resp, err := m.client.Do(req) if err != nil { return err @@ -87,8 +88,27 @@ func (m *GoogleChatManager) sendMessage(msg ChatMessage, threadKey string) error // If response is non 200, log and throw the error. if resp.StatusCode != http.StatusOK { - m.lo.WithField("status", resp.StatusCode).Error("Non OK HTTP Response received from Google Chat Webhook endpoint") - return errors.New("non ok response from gchat") + // Read the response body + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + // Log the error if unable to read the response body + m.lo.Error("Failed to read response body", "error", err) + return fmt.Errorf("failed to read response body") + } + // Ensure the original response body is closed + defer resp.Body.Close() + + // Convert the body bytes to a string for logging + responseBody := string(bodyBytes) + + // Log the status code and response body at the debug level + m.lo.Debug("Non OK HTTP Response received from Google Chat Webhook endpoint", "status", resp.StatusCode, "responseBody", responseBody) + + // Since the body has been read, if you need to use it later, + // you may need to reassign resp.Body with a new reader + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + return fmt.Errorf("non ok response from gchat") } return nil