Files
alertmanager-discord/cmd/root.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

103 lines
4.1 KiB
Go

package cmd
import (
"os"
"strings"
"time"
. "github.com/specklesystems/alertmanager-discord/pkg/flags"
"github.com/specklesystems/alertmanager-discord/pkg/server"
"github.com/specklesystems/alertmanager-discord/pkg/version"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const (
defaultConfigurationPath = "/etc/alertmanager-discord/config.yaml"
defaultMaxBackoffTimeSeconds = 10
)
var (
configurationFilePath string
webhookURL string
listenAddress string
maximumBackoffTimeSeconds int
)
func init() {
viper.SetDefault(ConfigurationPathFlagKey, defaultConfigurationPath)
viper.BindEnv(ConfigurationPathFlagKey, strings.ToUpper(ConfigurationPathFlagKey))
rootCmd.Flags().StringVarP(&configurationFilePath, ConfigurationPathFlagKey, "c", defaultConfigurationPath, "Path to the configuration file.")
viper.BindPFlag(ConfigurationPathFlagKey, rootCmd.Flags().Lookup(ConfigurationPathFlagKey))
viper.BindEnv(DiscordWebhookUrlFlagKey, strings.ToUpper(DiscordWebhookUrlFlagKey))
rootCmd.Flags().StringVarP(&webhookURL, DiscordWebhookUrlFlagKey, "d", "", "Url to the Discord webhook API endpoint.")
viper.BindPFlag(DiscordWebhookUrlFlagKey, rootCmd.Flags().Lookup(DiscordWebhookUrlFlagKey))
viper.SetDefault(ListenAddressFlagKey, server.DefaultListenAddress)
viper.BindEnv(ListenAddressFlagKey, strings.ToUpper(ListenAddressFlagKey))
rootCmd.Flags().StringVarP(&listenAddress, ListenAddressFlagKey, "l", "", "The address (host:port) which the server will attempt to bind to and listen on.")
viper.BindPFlag(ListenAddressFlagKey, rootCmd.Flags().Lookup(ListenAddressFlagKey))
viper.SetDefault(MaxBackoffTimeSecondsFlagKey, defaultMaxBackoffTimeSeconds)
viper.BindEnv(MaxBackoffTimeSecondsFlagKey, strings.ToUpper(MaxBackoffTimeSecondsFlagKey))
rootCmd.Flags().IntVarP(&maximumBackoffTimeSeconds, MaxBackoffTimeSecondsFlagKey, "", defaultMaxBackoffTimeSeconds, "The maximum elapsed duration (expressed as an integer number of seconds) to allow the Discord client to continue retrying to send messages to the Discord API.")
viper.BindPFlag(MaxBackoffTimeSecondsFlagKey, rootCmd.Flags().Lookup(MaxBackoffTimeSecondsFlagKey))
}
var rootCmd = &cobra.Command{
Use: "alertmanager-discord",
Version: version.Version,
Short: "Forwards AlertManager alerts to Discord.",
Long: `A simple web server that accepts AlertManager webhooks,
translates the data to match Discord's message specifications,
and forwards that to Discord's message API endpoint.`,
Run: func(cmd *cobra.Command, args []string) {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Debug().Msgf("Attempting to read from configuration file path: ('%s')", configurationFilePath)
viper.SetConfigFile(configurationFilePath)
if err := viper.ReadInConfig(); err != nil {
log.Info().Err(err).Msgf("Unable to read configuration file at path ('%s'). Attempting to parse command line arguments or environment variables, the command line argument has higher order of precedence.", configurationFilePath)
}
if viper.GetString(DiscordWebhookUrlFlagKey) != "" {
webhookURL = viper.GetString(DiscordWebhookUrlFlagKey)
}
if viper.GetString(ListenAddressFlagKey) != "" {
listenAddress = viper.GetString(ListenAddressFlagKey)
}
if viper.GetString(MaxBackoffTimeSecondsFlagKey) != "" {
maximumBackoffTimeSeconds = viper.GetInt(MaxBackoffTimeSecondsFlagKey)
}
amds := server.AlertManagerDiscordServer{
MaximumBackoffTimeSeconds: time.Duration(maximumBackoffTimeSeconds) * time.Second,
}
stopCh, err := amds.ListenAndServe(webhookURL, listenAddress)
defer func() {
if err = amds.Shutdown(); err != nil {
log.Fatal().Err(err).Msg("Error while shutting down server.")
}
}()
if err != nil {
log.Error().Err(err).Msg("Error in AlertManager-Discord server")
close(stopCh)
}
// Waits here for SIGINT (kill -2) or for channel to be closed (which can occur if there is an error in the server)
<-stopCh
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Error().Err(err).Msg("Error when executing command. Exiting program...")
os.Exit(1)
}
}