Skip to content

Commit

Permalink
feat(alerting): Implement alert-level provider overrides
Browse files Browse the repository at this point in the history
Fixes #96
  • Loading branch information
TwiN committed Dec 14, 2024
1 parent be9ae6f commit 23603a4
Show file tree
Hide file tree
Showing 48 changed files with 634 additions and 519 deletions.
35 changes: 19 additions & 16 deletions alerting/provider/awsses/awsses.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/logr"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
Expand All @@ -19,12 +20,7 @@ const (

// AlertProvider is the configuration necessary for sending an alert using AWS Simple Email Service
type AlertProvider struct {
AccessKeyID string `yaml:"access-key-id"`
SecretAccessKey string `yaml:"secret-access-key"`
Region string `yaml:"region"`

From string `yaml:"from"`
To string `yaml:"to"`
Config `yaml:",inline"`

// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
Expand All @@ -33,10 +29,19 @@ type AlertProvider struct {
Overrides []Override `yaml:"overrides,omitempty"`
}

type Config struct {
AccessKeyID string `yaml:"access-key-id"`
SecretAccessKey string `yaml:"secret-access-key"`
Region string `yaml:"region"`

From string `yaml:"from"`
To string `yaml:"to"`
}

// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
To string `yaml:"to"`
Group string `yaml:"group"`
Config `yaml:",inline"`
}

// IsValid returns whether the provider's configuration is valid
Expand Down Expand Up @@ -84,24 +89,22 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
},
Source: aws.String(provider.From),
}
_, err = svc.SendEmail(input)

if err != nil {
if _, err = svc.SendEmail(input); err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case ses.ErrCodeMessageRejected:
fmt.Println(ses.ErrCodeMessageRejected, aerr.Error())
logr.Error(ses.ErrCodeMessageRejected + ": " + aerr.Error())
case ses.ErrCodeMailFromDomainNotVerifiedException:
fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
logr.Error(ses.ErrCodeMailFromDomainNotVerifiedException + ": " + aerr.Error())
case ses.ErrCodeConfigurationSetDoesNotExistException:
fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
logr.Error(ses.ErrCodeConfigurationSetDoesNotExistException + ": " + aerr.Error())
default:
fmt.Println(aerr.Error())
logr.Error(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
logr.Error(err.Error())
}

return err
Expand Down
48 changes: 29 additions & 19 deletions alerting/provider/awsses/awsses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
if invalidProvider.IsValid() {
t.Error("provider shouldn't have been valid")
}
invalidProviderWithOneKey := AlertProvider{From: "[email protected]", To: "[email protected]", AccessKeyID: "1"}
invalidProviderWithOneKey := AlertProvider{Config: Config{From: "[email protected]", To: "[email protected]", AccessKeyID: "1"}}
if invalidProviderWithOneKey.IsValid() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{From: "[email protected]", To: "[email protected]"}
validProvider := AlertProvider{Config: Config{From: "[email protected]", To: "[email protected]"}}
if !validProvider.IsValid() {
t.Error("provider should've been valid")
}
validProviderWithKeys := AlertProvider{From: "[email protected]", To: "[email protected]", AccessKeyID: "1", SecretAccessKey: "1"}
validProviderWithKeys := AlertProvider{Config: Config{From: "[email protected]", To: "[email protected]", AccessKeyID: "1", SecretAccessKey: "1"}}
if !validProviderWithKeys.IsValid() {
t.Error("provider should've been valid")
}
Expand All @@ -30,8 +30,8 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
providerWithInvalidOverrideGroup := AlertProvider{
Overrides: []Override{
{
To: "[email protected]",
Group: "",
Config: Config{To: "[email protected]"},
Group: "",
},
},
}
Expand All @@ -41,21 +41,23 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
providerWithInvalidOverrideTo := AlertProvider{
Overrides: []Override{
{
To: "",
Group: "group",
Config: Config{To: ""},
Group: "group",
},
},
}
if providerWithInvalidOverrideTo.IsValid() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
From: "[email protected]",
To: "[email protected]",
Config: Config{
From: "[email protected]",
To: "[email protected]",
},
Overrides: []Override{
{
To: "[email protected]",
Group: "group",
Config: Config{To: "[email protected]"},
Group: "group",
},
},
}
Expand Down Expand Up @@ -134,7 +136,9 @@ func TestAlertProvider_getToForGroup(t *testing.T) {
{
Name: "provider-no-override-specify-no-group-should-default",
Provider: AlertProvider{
To: "[email protected]",
Config: Config{
To: "[email protected]",
},
Overrides: nil,
},
InputGroup: "",
Expand All @@ -143,7 +147,9 @@ func TestAlertProvider_getToForGroup(t *testing.T) {
{
Name: "provider-no-override-specify-group-should-default",
Provider: AlertProvider{
To: "[email protected]",
Config: Config{
To: "[email protected]",
},
Overrides: nil,
},
InputGroup: "group",
Expand All @@ -152,11 +158,13 @@ func TestAlertProvider_getToForGroup(t *testing.T) {
{
Name: "provider-with-override-specify-no-group-should-default",
Provider: AlertProvider{
To: "[email protected]",
Config: Config{
To: "[email protected]",
},
Overrides: []Override{
{
Group: "group",
To: "[email protected]",
Group: "group",
Config: Config{To: "[email protected]"},
},
},
},
Expand All @@ -166,11 +174,13 @@ func TestAlertProvider_getToForGroup(t *testing.T) {
{
Name: "provider-with-override-specify-group-should-override",
Provider: AlertProvider{
To: "[email protected]",
Config: Config{
To: "[email protected]",
},
Overrides: []Override{
{
Group: "group",
To: "[email protected]",
Group: "group",
Config: Config{To: "[email protected]"},
},
},
},
Expand Down
10 changes: 7 additions & 3 deletions alerting/provider/custom/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import (
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
// Technically, all alert providers should be reachable using the custom alert provider
type AlertProvider struct {
Config `yaml:",inline"`

// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
}

type Config struct {
URL string `yaml:"url"`
Method string `yaml:"method,omitempty"`
Body string `yaml:"body,omitempty"`
Expand All @@ -23,9 +30,6 @@ type AlertProvider struct {

// ClientConfig is the configuration of the client used to communicate with the provider's target
ClientConfig *client.Config `yaml:"client,omitempty"`

// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
}

// IsValid returns whether the provider's configuration is valid
Expand Down
38 changes: 23 additions & 15 deletions alerting/provider/custom/custom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import (

func TestAlertProvider_IsValid(t *testing.T) {
t.Run("invalid-provider", func(t *testing.T) {
invalidProvider := AlertProvider{URL: ""}
invalidProvider := AlertProvider{Config: Config{URL: ""}}
if invalidProvider.IsValid() {
t.Error("provider shouldn't have been valid")
}
})
t.Run("valid-provider", func(t *testing.T) {
validProvider := AlertProvider{URL: "https://example.com"}
validProvider := AlertProvider{Config: Config{URL: "https://example.com"}}
if validProvider.ClientConfig != nil {
t.Error("provider client config should have been nil prior to IsValid() being executed")
}
Expand Down Expand Up @@ -112,8 +112,10 @@ func TestAlertProvider_Send(t *testing.T) {

func TestAlertProvider_buildHTTPRequest(t *testing.T) {
customAlertProvider := &AlertProvider{
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]&url=[ENDPOINT_URL]",
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED]",
Config: Config{
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]&url=[ENDPOINT_URL]",
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED]",
},
}
alertDescription := "alert-description"
scenarios := []struct {
Expand Down Expand Up @@ -156,8 +158,10 @@ func TestAlertProvider_buildHTTPRequest(t *testing.T) {

func TestAlertProviderWithResultErrors_buildHTTPRequest(t *testing.T) {
customAlertWithErrorsProvider := &AlertProvider{
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]&url=[ENDPOINT_URL]&error=[RESULT_ERRORS]",
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED],[RESULT_ERRORS]",
Config: Config{
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]&url=[ENDPOINT_URL]&error=[RESULT_ERRORS]",
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED],[RESULT_ERRORS]",
},
}
alertDescription := "alert-description"
scenarios := []struct {
Expand Down Expand Up @@ -202,13 +206,15 @@ func TestAlertProviderWithResultErrors_buildHTTPRequest(t *testing.T) {

func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {
customAlertProvider := &AlertProvider{
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
Headers: nil,
Placeholders: map[string]map[string]string{
"ALERT_TRIGGERED_OR_RESOLVED": {
"RESOLVED": "fixed",
"TRIGGERED": "boom",
Config: Config{
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
Headers: nil,
Placeholders: map[string]map[string]string{
"ALERT_TRIGGERED_OR_RESOLVED": {
"RESOLVED": "fixed",
"TRIGGERED": "boom",
},
},
},
}
Expand Down Expand Up @@ -253,8 +259,10 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {

func TestAlertProvider_GetAlertStatePlaceholderValueDefaults(t *testing.T) {
customAlertProvider := &AlertProvider{
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
Config: Config{
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
},
}
if customAlertProvider.GetAlertStatePlaceholderValue(true) != "RESOLVED" {
t.Error("expected RESOLVED, got", customAlertProvider.GetAlertStatePlaceholderValue(true))
Expand Down
12 changes: 7 additions & 5 deletions alerting/provider/discord/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,24 @@ import (

// AlertProvider is the configuration necessary for sending an alert using Discord
type AlertProvider struct {
WebhookURL string `yaml:"webhook-url"`
Config `yaml:",inline"`

// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`

// Overrides is a list of Override that may be prioritized over the default configuration
Overrides []Override `yaml:"overrides,omitempty"`
}

// Title is the title of the message that will be sent
Title string `yaml:"title,omitempty"`
type Config struct {
WebhookURL string `yaml:"webhook-url"`
Title string `yaml:"title,omitempty"` // Title of the message that will be sent
}

// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
WebhookURL string `yaml:"webhook-url"`
Group string `yaml:"group"`
Config `yaml:",inline"`
}

// IsValid returns whether the provider's configuration is valid
Expand Down
Loading

0 comments on commit 23603a4

Please sign in to comment.