forked from TwiN/gatus
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(alerting): Add AWS SES Alerting Provider (TwiN#579)
* Add SES Provider * Formatting * Rename ses to aws-ses * Typo * Parse tag instead of type name * Use aws.slice to convert string array & rename awsses -> aws-ses * Rename type * Update README.md * Update alerting/config.go * Rename package aws-ses to awsses * Update README.md * PR comments --------- Co-authored-by: TwiN <[email protected]>
- Loading branch information
Showing
9 changed files
with
421 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga | |
- [Storage](#storage) | ||
- [Client configuration](#client-configuration) | ||
- [Alerting](#alerting) | ||
- [Configuring AWS SES alerts](#configuring-aws-ses-alerts) | ||
- [Configuring Discord alerts](#configuring-discord-alerts) | ||
- [Configuring Email alerts](#configuring-email-alerts) | ||
- [Configuring GitHub alerts](#configuring-github-alerts) | ||
|
@@ -1059,6 +1060,46 @@ endpoints: | |
``` | ||
|
||
|
||
#### Configuring AWS SES alerts | ||
| Parameter | Description | Default | | ||
|:-------------------------------------|:-------------------------------------------------------------------------------------------|:--------------| | ||
| `alerting.aws-ses` | Settings for alerts of type `aws-ses` | `{}` | | ||
| `alerting.aws-ses.access-key-id` | AWS Access Key ID | Optional `""` | | ||
| `alerting.aws-ses.secret-access-key` | AWS Secret Access Key | Optional `""` | | ||
| `alerting.aws-ses.region` | AWS Region | Required `""` | | ||
| `alerting.aws-ses.from` | The Email address to send the emails from (should be registered in SES) | Required `""` | | ||
| `alerting.aws-ses.to` | Comma separated list of email address to notify | Required `""` | | ||
| `alerting.aws-ses.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A | | ||
|
||
```yaml | ||
alerting: | ||
aws-ses: | ||
access-key-id: "..." | ||
secret-access-key: "..." | ||
region: "us-east-1" | ||
from: "[email protected]" | ||
to: "[email protected]" | ||
endpoints: | ||
- name: website | ||
interval: 30s | ||
url: "https://twin.sh/health" | ||
conditions: | ||
- "[STATUS] == 200" | ||
- "[BODY].status == UP" | ||
- "[RESPONSE_TIME] < 300" | ||
alerts: | ||
- type: aws-ses | ||
failure-threshold: 5 | ||
send-on-resolved: true | ||
description: "healthcheck failed" | ||
``` | ||
|
||
If the `access-key-id` and `secret-access-key` are not defined Gatus will fall back to IAM authentication. | ||
|
||
Make sure you have the ability to use `ses:SendEmail`. | ||
|
||
|
||
#### Configuring custom alerts | ||
| Parameter | Description | Default | | ||
|:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------| | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
package awsses | ||
|
||
import ( | ||
"fmt" | ||
"github.com/TwiN/gatus/v5/alerting/alert" | ||
"github.com/TwiN/gatus/v5/core" | ||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/aws/awserr" | ||
"github.com/aws/aws-sdk-go/aws/credentials" | ||
"github.com/aws/aws-sdk-go/aws/session" | ||
"github.com/aws/aws-sdk-go/service/ses" | ||
"strings" | ||
) | ||
|
||
const ( | ||
CharSet = "UTF-8" | ||
) | ||
|
||
// 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"` | ||
|
||
// 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"` | ||
} | ||
|
||
// Override is a case under which the default integration is overridden | ||
type Override struct { | ||
Group string `yaml:"group"` | ||
To string `yaml:"to"` | ||
} | ||
|
||
// IsValid returns whether the provider's configuration is valid | ||
func (provider *AlertProvider) IsValid() bool { | ||
registeredGroups := make(map[string]bool) | ||
if provider.Overrides != nil { | ||
for _, override := range provider.Overrides { | ||
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.To) == 0 { | ||
return false | ||
} | ||
registeredGroups[override.Group] = true | ||
} | ||
} | ||
|
||
// if both AccessKeyID and SecretAccessKey are specified, we'll use these to authenticate, | ||
// otherwise if neither are specified, then we'll fall back on IAM authentication. | ||
return len(provider.From) > 0 && len(provider.To) > 0 && | ||
((len(provider.AccessKeyID) == 0 && len(provider.SecretAccessKey) == 0) || (len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0)) | ||
} | ||
|
||
// Send an alert using the provider | ||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { | ||
sess, err := provider.CreateSesSession() | ||
if err != nil { | ||
return err | ||
} | ||
svc := ses.New(sess) | ||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved) | ||
emails := strings.Split(provider.getToForGroup(endpoint.Group), ",") | ||
|
||
input := &ses.SendEmailInput{ | ||
Destination: &ses.Destination{ | ||
ToAddresses: aws.StringSlice(emails), | ||
}, | ||
Message: &ses.Message{ | ||
Body: &ses.Body{ | ||
Text: &ses.Content{ | ||
Charset: aws.String(CharSet), | ||
Data: aws.String(body), | ||
}, | ||
}, | ||
Subject: &ses.Content{ | ||
Charset: aws.String(CharSet), | ||
Data: aws.String(subject), | ||
}, | ||
}, | ||
Source: aws.String(provider.From), | ||
} | ||
_, err = svc.SendEmail(input) | ||
|
||
if err != nil { | ||
if aerr, ok := err.(awserr.Error); ok { | ||
switch aerr.Code() { | ||
case ses.ErrCodeMessageRejected: | ||
fmt.Println(ses.ErrCodeMessageRejected, aerr.Error()) | ||
case ses.ErrCodeMailFromDomainNotVerifiedException: | ||
fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error()) | ||
case ses.ErrCodeConfigurationSetDoesNotExistException: | ||
fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error()) | ||
default: | ||
fmt.Println(aerr.Error()) | ||
} | ||
} else { | ||
// Print the error, cast err to awserr.Error to get the Code and | ||
// Message from an error. | ||
fmt.Println(err.Error()) | ||
} | ||
|
||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// buildMessageSubjectAndBody builds the message subject and body | ||
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) { | ||
var subject, message, results string | ||
if resolved { | ||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName()) | ||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) | ||
} else { | ||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName()) | ||
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) | ||
} | ||
for _, conditionResult := range result.ConditionResults { | ||
var prefix string | ||
if conditionResult.Success { | ||
prefix = "✅" | ||
} else { | ||
prefix = "❌" | ||
} | ||
results += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition) | ||
} | ||
var description string | ||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { | ||
description = "\n\nAlert description: " + alertDescription | ||
} | ||
return subject, message + description + "\n\nCondition results:\n" + results | ||
} | ||
|
||
// getToForGroup returns the appropriate email integration to for a given group | ||
func (provider *AlertProvider) getToForGroup(group string) string { | ||
if provider.Overrides != nil { | ||
for _, override := range provider.Overrides { | ||
if group == override.Group { | ||
return override.To | ||
} | ||
} | ||
} | ||
return provider.To | ||
} | ||
|
||
// GetDefaultAlert returns the provider's default alert configuration | ||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert { | ||
return provider.DefaultAlert | ||
} | ||
|
||
func (provider AlertProvider) CreateSesSession() (*session.Session, error) { | ||
config := &aws.Config{ | ||
Region: aws.String(provider.Region), | ||
} | ||
|
||
if len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0 { | ||
config.Credentials = credentials.NewStaticCredentials(provider.AccessKeyID, provider.SecretAccessKey, "") | ||
} | ||
|
||
return session.NewSession(config) | ||
} |
Oops, something went wrong.