Configurable logging levels (#16)

* Update screenshot to match current output
* Log level is configurable
* Alert name is provided in log line, where available.
* Capitalise Discord, check err in integration test, and other syntax issues
* Go formatting syntax fixes
* Log the common alert name or group alert name, in that precedence
This commit is contained in:
Iain Sproat
2022-11-21 21:04:05 +00:00
committed by GitHub
parent 6c6eef2854
commit f2872d6dea
12 changed files with 117 additions and 75 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 312 KiB

+14 -19
View File
@@ -10,7 +10,7 @@ This program is not a replacement to alertmanager, it accepts webhooks from aler
The standard "dataflow" should be: The standard "dataflow" should be:
``` ```text
Prometheus -------------> alertmanager -------------------> alertmanager-discord Prometheus -------------> alertmanager -------------------> alertmanager-discord
alerting: receivers: alerting: receivers:
@@ -18,11 +18,6 @@ alerting: receivers:
- static_configs: webhook_configs: - DISCORD_WEBHOOK=https://discordapp.com/api/we... - static_configs: webhook_configs: - DISCORD_WEBHOOK=https://discordapp.com/api/we...
- targets: - url: 'http://localhost:9094' - targets: - url: 'http://localhost:9094'
- 127.0.0.1:9093 - 127.0.0.1:9093
``` ```
## Features ## Features
@@ -44,30 +39,30 @@ alerting: receivers:
## Example alertmanager config ## Example alertmanager config
``` ```yaml
global: global:
# The smarthost and SMTP sender used for mail notifications. # The smarthost and SMTP sender used for mail notifications.
smtp_smarthost: 'localhost:25' smtp_smarthost: "localhost:25"
smtp_from: 'alertmanager@example.org' smtp_from: "alertmanager@example.org"
smtp_auth_username: 'alertmanager' smtp_auth_username: "alertmanager"
smtp_auth_password: 'password' smtp_auth_password: "password"
# The directory from which notification templates are read. # The directory from which notification templates are read.
templates: templates:
- '/etc/alertmanager/template/*.tmpl' - "/etc/alertmanager/template/*.tmpl"
# The root route on which each incoming alert enters. # The root route on which each incoming alert enters.
route: route:
group_by: ['alertname'] group_by: ["alertname"]
group_wait: 20s group_wait: 20s
group_interval: 5m group_interval: 5m
repeat_interval: 3h repeat_interval: 3h
receiver: discord_webhook receiver: discord_webhook
receivers: receivers:
- name: 'discord_webhook' - name: "discord_webhook"
webhook_configs: webhook_configs:
- url: 'http://localhost:9094' - url: "http://localhost:9094"
``` ```
## Deployment ## Deployment
@@ -88,9 +83,9 @@ discord_webhook_url: https://discord.com/api/webhooks/123456789123456789/abc
go run . --configuration_file_path=/path/to/your/config.yaml go run . --configuration_file_path=/path/to/your/config.yaml
``` ```
### Docker ### Docker or OCI-compatible container runtime
If you wish to deploy this to docker infra, you can find the docker hub repo here: https://hub.docker.com/r/speckle/alertmanager-discord/ If you wish to deploy this to Docker, or similar OCI-compatible container runtime, you can pull the OCI image from the [Docker Hub repository](https://hub.docker.com/r/speckle/alertmanager-discord/).
### Kubernetes Helm Chart ### Kubernetes Helm Chart
@@ -169,4 +164,4 @@ go test ./... -v -cover -test.shuffle on
## Acknowledgements ## Acknowledgements
This repository is forked from https://github.com/benjojo/alertmanager-discord under the Apache 2.0 license This repository is forked from [benjojo/alertmanager-discord](https://github.com/benjojo/alertmanager-discord) under the Apache 2.0 license
+49 -26
View File
@@ -5,7 +5,7 @@ import (
"strings" "strings"
"time" "time"
. "github.com/specklesystems/alertmanager-discord/pkg/flags" "github.com/specklesystems/alertmanager-discord/pkg/flags"
"github.com/specklesystems/alertmanager-discord/pkg/server" "github.com/specklesystems/alertmanager-discord/pkg/server"
"github.com/specklesystems/alertmanager-discord/pkg/version" "github.com/specklesystems/alertmanager-discord/pkg/version"
@@ -18,35 +18,30 @@ import (
const ( const (
defaultConfigurationPath = "/etc/alertmanager-discord/config.yaml" defaultConfigurationPath = "/etc/alertmanager-discord/config.yaml"
defaultMaxBackoffTimeSeconds = 10 defaultMaxBackoffTimeSeconds = 10
defaultLogLevel = "info"
) )
var ( var (
configurationFilePath string configurationFilePath string
webhookURL string webhookURL string
listenAddress string listenAddress string
logLevel string
maximumBackoffTimeSeconds int maximumBackoffTimeSeconds int
) )
func init() { func init() {
viper.SetDefault(ConfigurationPathFlagKey, defaultConfigurationPath) defineConfigurationVariable(&configurationFilePath, rootCmd.Flags().StringVarP, flags.ConfigurationPathFlagKey, "c", defaultConfigurationPath, "Path to the configuration file.")
defineConfigurationVariable(&webhookURL, rootCmd.Flags().StringVarP, flags.DiscordWebhookUrlFlagKey, "d", "", "Url to the Discord webhook API endpoint.")
defineConfigurationVariable(&listenAddress, rootCmd.Flags().StringVarP, flags.ListenAddressFlagKey, "l", server.DefaultListenAddress, "The address (host:port) which the server will attempt to bind to and listen on.")
defineConfigurationVariable(&logLevel, rootCmd.Flags().StringVarP, flags.LogLevelFlagKey, "", defaultLogLevel, "The minimum level of logging to be produced by the pod. Acceptable values, in ascending order, are 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic', or 'disabled'.")
defineConfigurationVariable(&maximumBackoffTimeSeconds, rootCmd.Flags().IntVarP, flags.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.BindEnv(ConfigurationPathFlagKey, strings.ToUpper(ConfigurationPathFlagKey)) func defineConfigurationVariable[K int | string](variable *K, flagParser func(*K, string, string, K, string), flagKey string, shorthand string, defaultValue K, description string) {
rootCmd.Flags().StringVarP(&configurationFilePath, ConfigurationPathFlagKey, "c", defaultConfigurationPath, "Path to the configuration file.") viper.SetDefault(flagKey, defaultValue)
viper.BindPFlag(ConfigurationPathFlagKey, rootCmd.Flags().Lookup(ConfigurationPathFlagKey)) viper.BindEnv(flagKey, strings.ToUpper(flagKey))
flagParser(variable, flagKey, shorthand, defaultValue, description)
viper.BindEnv(DiscordWebhookUrlFlagKey, strings.ToUpper(DiscordWebhookUrlFlagKey)) viper.BindPFlag(flagKey, rootCmd.Flags().Lookup(flagKey))
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{ var rootCmd = &cobra.Command{
@@ -57,22 +52,27 @@ var rootCmd = &cobra.Command{
translates the data to match Discord's message specifications, translates the data to match Discord's message specifications,
and forwards that to Discord's message API endpoint.`, and forwards that to Discord's message API endpoint.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix zerolog.TimeFieldFormat = time.RFC3339
zerolog.SetGlobalLevel(zerolog.InfoLevel)
// these log messages are generated before the log level is set
log.Debug().Msgf("Attempting to read from configuration file path: ('%s')", configurationFilePath) log.Debug().Msgf("Attempting to read from configuration file path: ('%s')", configurationFilePath)
viper.SetConfigFile(configurationFilePath) viper.SetConfigFile(configurationFilePath)
if err := viper.ReadInConfig(); err != nil { 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) 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) != "" { if viper.GetString(flags.DiscordWebhookUrlFlagKey) != "" {
webhookURL = viper.GetString(DiscordWebhookUrlFlagKey) webhookURL = viper.GetString(flags.DiscordWebhookUrlFlagKey)
} }
if viper.GetString(ListenAddressFlagKey) != "" { if viper.GetString(flags.ListenAddressFlagKey) != "" {
listenAddress = viper.GetString(ListenAddressFlagKey) listenAddress = viper.GetString(flags.ListenAddressFlagKey)
} }
if viper.GetString(MaxBackoffTimeSecondsFlagKey) != "" {
maximumBackoffTimeSeconds = viper.GetInt(MaxBackoffTimeSecondsFlagKey) setGlobalLogLevel(viper.GetString(flags.LogLevelFlagKey))
if viper.GetString(flags.MaxBackoffTimeSecondsFlagKey) != "" {
maximumBackoffTimeSeconds = viper.GetInt(flags.MaxBackoffTimeSecondsFlagKey)
} }
amds := server.AlertManagerDiscordServer{ amds := server.AlertManagerDiscordServer{
@@ -100,3 +100,26 @@ func Execute() {
os.Exit(1) os.Exit(1)
} }
} }
func setGlobalLogLevel(logLevel string) {
switch logLevel {
case "trace":
zerolog.SetGlobalLevel(zerolog.TraceLevel)
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case "fatal":
zerolog.SetGlobalLevel(zerolog.FatalLevel)
case "panic":
zerolog.SetGlobalLevel(zerolog.PanicLevel)
case "disabled":
zerolog.SetGlobalLevel(zerolog.Disabled)
default:
break
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ A Helm chart to deploy alertmanager-discord to Kubernetes
| securityContext.readOnlyRootFilesystem | bool | `true` | | | securityContext.readOnlyRootFilesystem | bool | `true` | |
| securityContext.runAsNonRoot | bool | `true` | | | securityContext.runAsNonRoot | bool | `true` | |
| securityContext.runAsUser | int | `1000` | | | securityContext.runAsUser | int | `1000` | |
| server.configuration.key | string | `"config.yaml"` | | | server.configuration.key | string | `"config.yaml"` | the key within the Kubernetes Secret. This key is expected to be a filename, as it will for the path for the configuration file when mounted to the container. |
| server.configuration.name | string | `"discord-config"` | name of the Kubernetes Secret containing the configuration file, will be mounted to the container. Must be in the same namespace as this helm chart is deployed. | | server.configuration.name | string | `"discord-config"` | name of the Kubernetes Secret containing the configuration file, will be mounted to the container. Must be in the same namespace as this helm chart is deployed. |
| service.port | int | `9094` | The port to which alertmanager should push alerts | | service.port | int | `9094` | The port to which alertmanager should push alerts |
| service.type | string | `"ClusterIP"` | | | service.type | string | `"ClusterIP"` | |
+1
View File
@@ -55,6 +55,7 @@ server:
configuration: configuration:
# -- name of the Kubernetes Secret containing the configuration file, will be mounted to the container. Must be in the same namespace as this helm chart is deployed. # -- name of the Kubernetes Secret containing the configuration file, will be mounted to the container. Must be in the same namespace as this helm chart is deployed.
name: discord-config name: discord-config
# -- the key within the Kubernetes Secret. This key is expected to be a filename, as it will for the path for the configuration file when mounted to the container.
key: config.yaml key: config.yaml
# within the config.yaml data, it should be yaml formatted with the key `discord_webhook_url`, and optionally keys `listen_address` & `max_backoff_time_seconds`. An example of the data expected can be found at ./test/test-config.yaml # within the config.yaml data, it should be yaml formatted with the key `discord_webhook_url`, and optionally keys `listen_address` & `max_backoff_time_seconds`. An example of the data expected can be found at ./test/test-config.yaml
+28 -15
View File
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"time" "time"
"github.com/specklesystems/alertmanager-discord/pkg/alertmanager" "github.com/specklesystems/alertmanager-discord/pkg/alertmanager"
@@ -13,6 +14,7 @@ import (
"github.com/specklesystems/alertmanager-discord/pkg/prometheus" "github.com/specklesystems/alertmanager-discord/pkg/prometheus"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -24,9 +26,9 @@ type AlertForwarderHandler struct {
af AlertForwarder af AlertForwarder
} }
func NewAlertForwarderHandler(client *http.Client, webhookURL string, maximumBackoffTimeSeconds time.Duration) *AlertForwarderHandler { func NewAlertForwarderHandler(client *http.Client, webhookURL string, maximumBackoffElapsedTime time.Duration) *AlertForwarderHandler {
return &AlertForwarderHandler{ return &AlertForwarderHandler{
af: NewAlertForwarder(client, webhookURL, maximumBackoffTimeSeconds), af: NewAlertForwarder(client, webhookURL, maximumBackoffElapsedTime),
} }
} }
@@ -38,12 +40,20 @@ type AlertForwarder struct {
client *discord.Client client *discord.Client
} }
func NewAlertForwarder(client *http.Client, webhookURL string, maximumBackoffTimeSeconds time.Duration) AlertForwarder { func NewAlertForwarder(client *http.Client, webhookURL string, maximumBackoffElapsedTime time.Duration) AlertForwarder {
return AlertForwarder{ return AlertForwarder{
client: discord.NewClient(client, webhookURL, maximumBackoffTimeSeconds), client: discord.NewClient(client, webhookURL, maximumBackoffElapsedTime),
} }
} }
func (af *AlertForwarder) groupAlerts(amo *alertmanager.Out) map[string][]alertmanager.Alert {
groupedAlerts := make(map[string][]alertmanager.Alert)
for _, alert := range amo.Alerts {
groupedAlerts[alert.Status] = append(groupedAlerts[alert.Status], alert)
}
return groupedAlerts
}
func (af *AlertForwarder) sendWebhook(correlationId string, amo *alertmanager.Out, w http.ResponseWriter) { func (af *AlertForwarder) sendWebhook(correlationId string, amo *alertmanager.Out, w http.ResponseWriter) {
if len(amo.Alerts) < 1 { if len(amo.Alerts) < 1 {
log.Debug(). log.Debug().
@@ -53,31 +63,35 @@ func (af *AlertForwarder) sendWebhook(correlationId string, amo *alertmanager.Ou
return return
} }
groupedAlerts := make(map[string][]alertmanager.Alert) logger := zerolog.New(os.Stderr).With().
for _, alert := range amo.Alerts { Timestamp().
groupedAlerts[alert.Status] = append(groupedAlerts[alert.Status], alert) Str(logging.FieldKeyCorrelationId, correlationId).Logger()
if amo.CommonLabels.Alertname != "" {
logger = logger.With().Str(logging.FieldKeyAlertName, amo.CommonLabels.Alertname).Logger()
} else if amo.GroupLabels.Alertname != "" {
logger = logger.With().Str(logging.FieldKeyAlertName, amo.GroupLabels.Alertname).Logger()
} }
failedToPublishAtLeastOne := false failedToPublishAtLeastOne := false
for status, alerts := range groupedAlerts { for status, alerts := range af.groupAlerts(amo) {
DO := TranslateAlertManagerToDiscord(status, amo, alerts) DO := TranslateAlertManagerToDiscord(status, amo, alerts)
log.Info(). logger.Info().
Str(logging.FieldKeyEventType, logging.EventTypeRequestSending). Str(logging.FieldKeyEventType, logging.EventTypeRequestSending).
Str(logging.FieldKeyCorrelationId, correlationId). Str(logging.FieldKeyCorrelationId, correlationId).
Msg("Sending HTTP request to Discord.") Msg("Sending HTTP request to Discord.")
res, err := af.client.PublishMessage(DO) res, err := af.client.PublishMessage(DO)
if err != nil { if err != nil {
err = fmt.Errorf("Error encountered when publishing message to discord: %w", err) err = fmt.Errorf("failed to publish message to Discord: %w", err)
log.Error(). logger.Error().
Str(logging.FieldKeyCorrelationId, correlationId). Str(logging.FieldKeyCorrelationId, correlationId).
Err(err). Err(err).
Msg("Error when attempting to publish message to discord.") Msg("Error when attempting to publish message to Discord.")
failedToPublishAtLeastOne = true failedToPublishAtLeastOne = true
continue continue
} }
log.Info(). logger.Info().
Str(logging.FieldKeyEventType, logging.EventTypeResponseReceived). Str(logging.FieldKeyEventType, logging.EventTypeResponseReceived).
Str(logging.FieldKeyCorrelationId, correlationId). Str(logging.FieldKeyCorrelationId, correlationId).
Msg("HTTP response received from Discord") Msg("HTTP response received from Discord")
@@ -124,7 +138,7 @@ or https://prometheus.io/docs/alerting/latest/configuration/#webhook_config`
Msg("Sending HTTP request to Discord.") Msg("Sending HTTP request to Discord.")
res, err := af.client.PublishMessage(DO) res, err := af.client.PublishMessage(DO)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error encountered when publishing message to discord: %w", err) return nil, fmt.Errorf("error encountered when publishing message to Discord: %w", err)
} }
log.Info(). log.Info().
@@ -204,5 +218,4 @@ func (af *AlertForwarder) handleInvalidInput(correlationId string, b []byte, w h
} }
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return
} }
+3 -6
View File
@@ -17,13 +17,10 @@ func CheckWebhookURL(webhookURL string) (bool, *url.URL, error) {
parsedUrl, err := url.Parse(webhookURL) parsedUrl, err := url.Parse(webhookURL)
if err != nil { if err != nil {
return false, &url.URL{}, fmt.Errorf("The Discord WebHook URL ('%s') cannot be parsed as a url: %w", webhookURL, err) return false, &url.URL{}, fmt.Errorf("the Discord WebHook URL ('%s') cannot be parsed as a url: %w", webhookURL, err)
} }
host, _, err := net.SplitHostPort(parsedUrl.Host) host, _, _ := net.SplitHostPort(parsedUrl.Host)
if err != nil {
// return false, parsedUrl, fmt.Errorf("The Discord WebHook URL ('%s') host ('%s') cannot be separated into domain/ip and port components: %w", webhookURL, parsedUrl.Host, err)
}
if host == "" { if host == "" {
host = parsedUrl.Host host = parsedUrl.Host
} }
@@ -37,7 +34,7 @@ func CheckWebhookURL(webhookURL string) (bool, *url.URL, error) {
ok := re.Match([]byte(webhookURL)) ok := re.Match([]byte(webhookURL))
if !ok { if !ok {
return false, parsedUrl, fmt.Errorf("The Discord WebHook URL doesn't seem to be a valid Discord Webhook API url: '%s'", webhookURL) return false, parsedUrl, fmt.Errorf("the Discord WebHook URL doesn't seem to be a valid Discord Webhook API url: '%s'", webhookURL)
} }
return ok, parsedUrl, nil return ok, parsedUrl, nil
+3 -3
View File
@@ -8,17 +8,17 @@ import (
var ( var (
RequestsToDiscordInFlight = promauto.NewGauge(prometheus.GaugeOpts{ RequestsToDiscordInFlight = promauto.NewGauge(prometheus.GaugeOpts{
Name: "discord_client_requests_in_flight", Name: "discord_client_requests_in_flight",
Help: "The current number of http requests being sent by the discord client.", Help: "The current number of http requests being sent by the Discord client.",
}) })
RequestsToDiscordTotal = promauto.NewCounterVec(prometheus.CounterOpts{ RequestsToDiscordTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "discord_client_requests_total", Name: "discord_client_requests_total",
Help: "The total number of http requests sent by the discord client.", Help: "The total number of http requests sent by the Discord client.",
}, []string{"code", "method"}) }, []string{"code", "method"})
RequestsToDiscordDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ RequestsToDiscordDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "discord_client_request_duration_seconds", Name: "discord_client_request_duration_seconds",
Help: "Duration of all http requests sent by the discord client.", Help: "Duration of all http requests sent by the Discord client.",
Buckets: prometheus.DefBuckets, Buckets: prometheus.DefBuckets,
}, []string{"code"}) }, []string{"code"})
) )
+1
View File
@@ -5,4 +5,5 @@ const (
DiscordWebhookUrlFlagKey = "discord_webhook_url" DiscordWebhookUrlFlagKey = "discord_webhook_url"
ListenAddressFlagKey = "listen_address" ListenAddressFlagKey = "listen_address"
MaxBackoffTimeSecondsFlagKey = "max_backoff_time_seconds" MaxBackoffTimeSecondsFlagKey = "max_backoff_time_seconds"
LogLevelFlagKey = "log_level"
) )
+1
View File
@@ -6,6 +6,7 @@ const (
FieldKeyHttpMethod = "method" FieldKeyHttpMethod = "method"
FieldKeyHttpPath = "path" FieldKeyHttpPath = "path"
FieldKeyEventType = "event_type" FieldKeyEventType = "event_type"
FieldKeyAlertName = "alert_name"
FieldKeyCorrelationId = "correlation_id" FieldKeyCorrelationId = "correlation_id"
FieldKeyStatusCode = "status_code" FieldKeyStatusCode = "status_code"
) )
+13 -4
View File
@@ -22,11 +22,11 @@ const (
) )
func Test_Serve_HappyPath(t *testing.T) { func Test_Serve_HappyPath(t *testing.T) {
// create a mock discord server to respond to our request // create a mock Discord server to respond to our request
receivedRequest := make(chan bool, 1) receivedRequest := make(chan bool, 1)
mockDiscordServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mockDiscordServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Discord mock server will always return with Status Code 200 OK // Discord mock server will always return with Status Code 200 OK
receivedRequest <- true // notify the channel that the discord server received the request receivedRequest <- true // notify the channel that the Discord server received the request
})) }))
defer mockDiscordServer.Close() defer mockDiscordServer.Close()
@@ -44,19 +44,23 @@ func Test_Serve_HappyPath(t *testing.T) {
} }
res, err := client.Get(fmt.Sprintf("http://%s/liveness", serverListenAddress)) res, err := client.Get(fmt.Sprintf("http://%s/liveness", serverListenAddress))
assert.NoError(t, err)
assert.NotNil(t, res, "response to GET '/liveness' should not be nil") assert.NotNil(t, res, "response to GET '/liveness' should not be nil")
assert.Equal(t, http.StatusOK, res.StatusCode, "GET liveness should return status code OK (200)") assert.Equal(t, http.StatusOK, res.StatusCode, "GET liveness should return status code OK (200)")
res, err = client.Get(fmt.Sprintf("http://%s/readiness", serverListenAddress)) res, err = client.Get(fmt.Sprintf("http://%s/readiness", serverListenAddress))
assert.NoError(t, err)
assert.NotNil(t, res, "response to GET '/readiness' should not be nil") assert.NotNil(t, res, "response to GET '/readiness' should not be nil")
assert.Equal(t, http.StatusOK, res.StatusCode, "GET readiness should return status code OK (200)") assert.Equal(t, http.StatusOK, res.StatusCode, "GET readiness should return status code OK (200)")
res, err = client.Get(fmt.Sprintf("http://%s/favicon.ico", serverListenAddress)) res, err = client.Get(fmt.Sprintf("http://%s/favicon.ico", serverListenAddress))
assert.NoError(t, err)
assert.NotNil(t, res, "response to GET '/favicon.ico' should not be nil") assert.NotNil(t, res, "response to GET '/favicon.ico' should not be nil")
assert.Equal(t, http.StatusOK, res.StatusCode, "GET favicon.ico should return status code OK (200)") assert.Equal(t, http.StatusOK, res.StatusCode, "GET favicon.ico should return status code OK (200)")
res, err = client.Get(fmt.Sprintf("http://%s/metrics", serverListenAddress)) res, err = client.Get(fmt.Sprintf("http://%s/metrics", serverListenAddress))
assert.NoError(t, err)
assert.NotNil(t, res, "response to GET '/metrics' should not be nil") assert.NotNil(t, res, "response to GET '/metrics' should not be nil")
assert.Equal(t, http.StatusOK, res.StatusCode, "GET favicon.ico should return status code OK (200)") assert.Equal(t, http.StatusOK, res.StatusCode, "GET favicon.ico should return status code OK (200)")
// assert mock discord server received expected json // assert mock Discord server received expected json
ao := alertmanager.Out{ ao := alertmanager.Out{
Alerts: []alertmanager.Alert{ Alerts: []alertmanager.Alert{
{ {
@@ -68,6 +72,11 @@ func Test_Serve_HappyPath(t *testing.T) {
}{ }{
Summary: "a_common_annotation_summary", Summary: "a_common_annotation_summary",
}, },
GroupLabels: struct {
Alertname string `json:"alertname"`
}{
Alertname: "testAlertName",
},
} }
aoJson, err := json.Marshal(ao) aoJson, err := json.Marshal(ao)
@@ -86,7 +95,7 @@ func Test_Serve_HappyPath(t *testing.T) {
assert.NoError(t, err, "sending request to alertmanager-discord server.") assert.NoError(t, err, "sending request to alertmanager-discord server.")
assert.NotNil(t, res, "response to POST '/' should not be nil") assert.NotNil(t, res, "response to POST '/' should not be nil")
assert.Equal(t, http.StatusOK, res.StatusCode, "sending valid alertmanager data should expect http response status code") assert.Equal(t, http.StatusOK, res.StatusCode, "sending valid alertmanager data should expect http response status code")
assert.True(t, <-receivedRequest, "Mock discord server should have received response") // will wait until the request is received assert.True(t, <-receivedRequest, "Mock Discord server should have received response") // will wait until the request is received
// TODO assert log lines were generated // TODO assert log lines were generated
+3 -1
View File
@@ -15,7 +15,9 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const DefaultListenAddress = "0.0.0.0:9094" const (
DefaultListenAddress = "0.0.0.0:9094"
)
const ( const (
FaviconPath = "/favicon.ico" FaviconPath = "/favicon.ico"
LivenessPath = "/liveness" LivenessPath = "/liveness"