b4a48fd928
* Adds development instructions to README * Replaces deprecated io/ioutil with io package * Catch all thrown errors and handle them. - not catching errors could result in unknown behaviour * Fix gofmt formatting issues * Refactor to allow http client to be provided - default client does not have timeout etc., we may instead wish to provide a custom http client. * Refactor to something closer to the standard go layout - separates alert forwarder into separate package to allow for testing/reuse * Split out types, and split Discord client into its own package * Renaming of symbols for readability - no need to abbreviate words in modern IDEs * remove go-vet hook as it is broken when go files are not in root directory * unit tests for ~90% coverage * Update picture in README * Return error status codes to caller in event of error from Discord * Remove panic, replace with error status code response and log message. Improve the status codes that are returned to provide more context on what has occurred. * Graceful shutdown of server, including signal handling * Integration tests - mocks Discord server - tests Happy case and a couple of unhappy cases - most edge conditions are otherwise tested in unit tests * CheckWebHook should return errors instead of logging - additional checks in tests for nil objects - attempt to solve integration test pollution by using different port numbers to prevent potential collision - Temporarily comment out test causing interaction pollution with other tests * structured logging * feat(exponential backoff): Added to Discord client * Serve prometheus metrics - Monitoring for the discord client * adds correlation ID to logging * refactors the mock http client to allow it to work with instrumentation for monitoring * Helm chart service monitor * Improved flag and env var parsing * Application version passed in via build args * Order of precedence of configuration configuration file<environment variable<command line * Mounts secret to file instead of in environment variable * Adds build tag to integration tests to prevent them being run as a unit test
118 lines
3.3 KiB
Go
118 lines
3.3 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"time"
|
|
|
|
"github.com/specklesystems/alertmanager-discord/pkg/alertforwarder"
|
|
"github.com/specklesystems/alertmanager-discord/pkg/metrics"
|
|
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
const DefaultListenAddress = "0.0.0.0:9094"
|
|
const (
|
|
FaviconPath = "/favicon.ico"
|
|
LivenessPath = "/liveness"
|
|
ReadinessPath = "/readiness"
|
|
)
|
|
|
|
type AlertManagerDiscordServer struct {
|
|
httpServer *http.Server
|
|
MaximumBackoffTimeSeconds time.Duration
|
|
}
|
|
|
|
func (amds *AlertManagerDiscordServer) ListenAndServe(webhookUrl, listenAddress string) (chan os.Signal, error) {
|
|
stop := make(chan os.Signal, 1)
|
|
mux := http.NewServeMux()
|
|
|
|
ok, _, err := alertforwarder.CheckWebhookURL(webhookUrl)
|
|
if !ok {
|
|
return stop, fmt.Errorf("url is invalid: %w", err)
|
|
}
|
|
|
|
if listenAddress == "" {
|
|
log.Info().Msgf("Listen address not provided. Using default: '%s'", DefaultListenAddress)
|
|
listenAddress = DefaultListenAddress
|
|
}
|
|
log.Info().Msgf("Listening on: %s", listenAddress)
|
|
|
|
discordClient := &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
|
|
transformAndForwardWithInstrumentation := promhttp.InstrumentHandlerDuration(metrics.RequestsToAlertForwarderDuration,
|
|
promhttp.InstrumentHandlerCounter(metrics.RequestsToAlertForwarderTotal,
|
|
promhttp.InstrumentHandlerInFlight(metrics.RequestsToAlertForwarderInFlight,
|
|
alertforwarder.NewAlertForwarderHandler(discordClient,
|
|
webhookUrl,
|
|
amds.MaximumBackoffTimeSeconds,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
mux.HandleFunc("/", transformAndForwardWithInstrumentation)
|
|
|
|
mux.HandleFunc("/readiness", func(w http.ResponseWriter, r *http.Request) {
|
|
log.Info().Msg("Readiness probe encountered.")
|
|
})
|
|
|
|
mux.HandleFunc("/liveness", func(w http.ResponseWriter, r *http.Request) {
|
|
log.Info().Msg("Liveness probe encountered.")
|
|
})
|
|
|
|
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
|
// purposefully empty
|
|
})
|
|
|
|
mux.Handle("/metrics", promhttp.Handler())
|
|
|
|
amds.httpServer = &http.Server{
|
|
Addr: listenAddress,
|
|
Handler: mux,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
MaxHeaderBytes: 1 << 20,
|
|
}
|
|
|
|
// Setting up signal capturing
|
|
signal.Notify(stop, os.Interrupt)
|
|
|
|
go func() {
|
|
// check for nil prevents race condition if we have already shutdown the server before this goroutine attempts to start
|
|
if amds.httpServer != nil {
|
|
if err := amds.httpServer.ListenAndServe(); err != nil {
|
|
close(stop)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return stop, nil
|
|
}
|
|
|
|
func (amds *AlertManagerDiscordServer) Shutdown() error {
|
|
log.Info().Msg("Received signal to shut down server. Shutting down server...")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if amds.httpServer == nil {
|
|
// http server is not referenced, or was never created, so we're unable to shut it down
|
|
return nil
|
|
}
|
|
|
|
if err := amds.httpServer.Shutdown(ctx); err != nil {
|
|
// prevent race condition if shutdown signal was sent prior to server starting, we remove server reference to prevent it starting
|
|
amds.httpServer = nil
|
|
return err
|
|
}
|
|
|
|
// prevent race condition if shutdown signal was sent prior to server starting, we remove server remove to prevent it starting
|
|
amds.httpServer = nil
|
|
return nil
|
|
}
|