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
103 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|