-
-
Notifications
You must be signed in to change notification settings - Fork 516
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
Changes from 13 commits
8c94494
fb0dec6
504bfb4
19010f8
5aadae7
09b8b41
905e5d2
6d651e8
d84932c
e1572bb
0b24d79
0b1c744
623c64e
2fb5cd3
27c7f7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
|
@@ -1044,6 +1045,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 | | ||
|:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------| | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
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 PagerDuty | ||
beschoenen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AWS access key and secret are optional variables if you use IAM authentication. So they both should either be empty or filled in. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, should the condition check for IAM auth if the keys aren't provided? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IAM authentication is implicit on resources within AWS, and doesn't really need to be checked, if that's what you mean. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see, I was interpretting it as the aws profile config but I suppose it's implicit if you deploy to aws. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a comment that clarifies this? i.e. `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 auth |
||
} | ||
|
||
// Send an alert using the provider | ||
// | ||
// Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/ | ||
beschoenen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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), ",") | ||
|
||
beschoenen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
|
||
beschoenen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.