-
-
Notifications
You must be signed in to change notification settings - Fork 526
feat(alerting): Add AWS SES Alerting Provider #579
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
8c94494
Add SES Provider
beschoenen fb0dec6
Formatting
beschoenen 504bfb4
Rename ses to aws-ses
beschoenen 19010f8
Typo
beschoenen 5aadae7
Parse tag instead of type name
beschoenen 09b8b41
Use aws.slice to convert string array & rename awsses -> aws-ses
beschoenen 905e5d2
Rename type
beschoenen 6d651e8
Update README.md
TwiN d84932c
Update alerting/config.go
TwiN e1572bb
Merge branch 'master' into master
TwiN 0b24d79
Rename package aws-ses to awsses
beschoenen 0b1c744
Merge remote-tracking branch 'twin/master'
beschoenen 623c64e
Update README.md
TwiN 2fb5cd3
PR comments
beschoenen 27c7f7e
Merge remote-tracking branch 'twin/master'
beschoenen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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) | ||
|
@@ -1041,6 +1042,44 @@ 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" | ||
``` | ||
|
||
Make sure you have the ability to use `ses:SendEmail`. | ||
|
||
|
||
#### Configuring custom alerts | ||
| Parameter | Description | Default | | ||
|:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------| | ||
|
This file contains hidden or 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 hidden or 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 hidden or 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,167 @@ | ||
package aws_ses | ||
beschoenen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 PagerDuty | ||
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 | ||
} | ||
} | ||
|
||
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 | ||
// | ||
// Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/ | ||
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.