Files
alertmanager-discord/pkg/server/server.go
T
Iain Sproat b4a48fd928 Refactor and productionise (#6)
* 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
2022-11-14 09:46:27 +00:00

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
}