Skip to content

Commit 488e5da

Browse files
authored
Merge branch 'master' into add_conditions_to_custom_alerting
2 parents 67b591b + 5c78bd9 commit 488e5da

File tree

22 files changed

+864
-144
lines changed

22 files changed

+864
-144
lines changed

README.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
7171
- [Configuring Matrix alerts](#configuring-matrix-alerts)
7272
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
7373
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
74+
- [Configuring n8n alerts](#configuring-n8n-alerts)
7475
- [Configuring New Relic alerts](#configuring-new-relic-alerts)
7576
- [Configuring Ntfy alerts](#configuring-ntfy-alerts)
7677
- [Configuring Opsgenie alerts](#configuring-opsgenie-alerts)
@@ -745,6 +746,9 @@ endpoints:
745746
- "[STATUS] == 200"
746747
```
747748

749+
> ⚠️ **WARNING**:: Tunneling may introduce additional latency, especially if the connection to the tunnel is retried frequently.
750+
> This may lead to inaccurate response time measurements.
751+
748752

749753
### Alerting
750754
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
@@ -814,6 +818,7 @@ endpoints:
814818
| `alerting.matrix` | Configuration for alerts of type `matrix`. <br />See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` |
815819
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
816820
| `alerting.messagebird` | Configuration for alerts of type `messagebird`. <br />See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
821+
| `alerting.n8n` | Configuration for alerts of type `n8n`. <br />See [Configuring n8n alerts](#configuring-n8n-alerts). | `{}` |
817822
| `alerting.newrelic` | Configuration for alerts of type `newrelic`. <br />See [Configuring New Relic alerts](#configuring-new-relic-alerts). | `{}` |
818823
| `alerting.ntfy` | Configuration for alerts of type `ntfy`. <br />See [Configuring Ntfy alerts](#configuring-ntfy-alerts). | `{}` |
819824
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie`. <br />See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` |
@@ -1576,8 +1581,8 @@ alerting:
15761581
region: "US" # or "EU" for European region
15771582
15781583
endpoints:
1579-
- name: website
1580-
url: "https://twin.sh/health"
1584+
- name: example
1585+
url: "https://example.org"
15811586
interval: 5m
15821587
conditions:
15831588
- "[STATUS] == 200"
@@ -1587,6 +1592,50 @@ endpoints:
15871592
```
15881593

15891594

1595+
#### Configuring n8n alerts
1596+
| Parameter | Description | Default |
1597+
|:---------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
1598+
| `alerting.n8n` | Configuration for alerts of type `n8n` | `{}` |
1599+
| `alerting.n8n.webhook-url` | n8n webhook URL | Required `""` |
1600+
| `alerting.n8n.title` | Title of the alert sent to n8n | `""` |
1601+
| `alerting.n8n.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
1602+
| `alerting.n8n.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
1603+
| `alerting.n8n.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
1604+
| `alerting.n8n.overrides[].*` | See `alerting.n8n.*` parameters | `{}` |
1605+
1606+
[n8n](https://n8n.io/) is a workflow automation platform that allows you to automate tasks across different applications and services using webhooks.
1607+
1608+
Example:
1609+
```yaml
1610+
alerting:
1611+
n8n:
1612+
webhook-url: "https://your-n8n-instance.com/webhook/your-webhook-id"
1613+
title: "Gatus Monitoring"
1614+
default-alert:
1615+
send-on-resolved: true
1616+
1617+
endpoints:
1618+
- name: example
1619+
url: "https://example.org"
1620+
interval: 5m
1621+
conditions:
1622+
- "[STATUS] == 200"
1623+
alerts:
1624+
- type: n8n
1625+
description: "Health check alert"
1626+
```
1627+
1628+
The JSON payload sent to the n8n webhook will include:
1629+
- `title`: The configured title
1630+
- `endpoint_name`: Name of the endpoint
1631+
- `endpoint_group`: Group of the endpoint (if any)
1632+
- `endpoint_url`: URL being monitored
1633+
- `alert_description`: Custom alert description
1634+
- `resolved`: Boolean indicating if the alert is resolved
1635+
- `message`: Human-readable alert message
1636+
- `condition_results`: Array of condition results with their success status
1637+
1638+
15901639
#### Configuring Ntfy alerts
15911640
| Parameter | Description | Default |
15921641
|:-------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------|
@@ -2983,12 +3032,13 @@ endpoints:
29833032
password: "password"
29843033
body: |
29853034
{
2986-
"command": "uptime"
3035+
"command": "echo '{\"memory\": {\"used\": 512}}'"
29873036
}
29883037
interval: 1m
29893038
conditions:
29903039
- "[CONNECTED] == true"
29913040
- "[STATUS] == 0"
3041+
- "[BODY].memory.used > 500"
29923042
```
29933043

29943044
you can also use no authentication to monitor the endpoint by not specifying the username
@@ -3011,6 +3061,7 @@ endpoints:
30113061
The following placeholders are supported for endpoints of type SSH:
30123062
- `[CONNECTED]` resolves to `true` if the SSH connection was successful, `false` otherwise
30133063
- `[STATUS]` resolves the exit code of the command executed on the remote server (e.g. `0` for success)
3064+
- `[BODY]` resolves to the stdout output of the command executed on the remote server
30143065
- `[IP]` resolves to the IP address of the server
30153066
- `[RESPONSE_TIME]` resolves to the time it took to establish the connection and execute the command
30163067

alerting/alert/type.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ const (
6565
// TypeNewRelic is the Type for the newrelic alerting provider
6666
TypeNewRelic Type = "newrelic"
6767

68+
// TypeN8N is the Type for the n8n alerting provider
69+
TypeN8N Type = "n8n"
70+
6871
// TypeNtfy is the Type for the ntfy alerting provider
6972
TypeNtfy Type = "ntfy"
7073

alerting/config.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
2626
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
2727
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
28+
"github.com/TwiN/gatus/v5/alerting/provider/n8n"
2829
"github.com/TwiN/gatus/v5/alerting/provider/newrelic"
2930
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
3031
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
@@ -66,7 +67,6 @@ type Config struct {
6667
// Email is the configuration for the email alerting provider
6768
Email *email.AlertProvider `yaml:"email,omitempty"`
6869

69-
7070
// GitHub is the configuration for the github alerting provider
7171
GitHub *github.AlertProvider `yaml:"github,omitempty"`
7272

@@ -81,13 +81,13 @@ type Config struct {
8181

8282
// Gotify is the configuration for the gotify alerting provider
8383
Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"`
84-
84+
8585
// HomeAssistant is the configuration for the homeassistant alerting provider
8686
HomeAssistant *homeassistant.AlertProvider `yaml:"homeassistant,omitempty"`
8787

8888
// IFTTT is the configuration for the ifttt alerting provider
8989
IFTTT *ifttt.AlertProvider `yaml:"ifttt,omitempty"`
90-
90+
9191
// Ilert is the configuration for the ilert alerting provider
9292
Ilert *ilert.AlertProvider `yaml:"ilert,omitempty"`
9393

@@ -112,6 +112,9 @@ type Config struct {
112112
// NewRelic is the configuration for the newrelic alerting provider
113113
NewRelic *newrelic.AlertProvider `yaml:"newrelic,omitempty"`
114114

115+
// N8N is the configuration for the n8n alerting provider
116+
N8N *n8n.AlertProvider `yaml:"n8n,omitempty"`
117+
115118
// Ntfy is the configuration for the ntfy alerting provider
116119
Ntfy *ntfy.AlertProvider `yaml:"ntfy,omitempty"`
117120

alerting/provider/awsses/awsses.go

Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package awsses
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"strings"
78

89
"github.com/TwiN/gatus/v5/alerting/alert"
910
"github.com/TwiN/gatus/v5/config/endpoint"
10-
"github.com/TwiN/logr"
11-
"github.com/aws/aws-sdk-go/aws"
12-
"github.com/aws/aws-sdk-go/aws/awserr"
13-
"github.com/aws/aws-sdk-go/aws/credentials"
14-
"github.com/aws/aws-sdk-go/aws/session"
15-
"github.com/aws/aws-sdk-go/service/ses"
11+
"github.com/aws/aws-sdk-go-v2/aws"
12+
"github.com/aws/aws-sdk-go-v2/config"
13+
"github.com/aws/aws-sdk-go-v2/credentials"
14+
"github.com/aws/aws-sdk-go-v2/service/ses"
15+
"github.com/aws/aws-sdk-go-v2/service/ses/types"
1616
"gopkg.in/yaml.v3"
1717
)
1818

@@ -102,63 +102,50 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
102102
if err != nil {
103103
return err
104104
}
105-
awsSession, err := provider.createSession(cfg)
105+
ctx := context.Background()
106+
svc, err := provider.createClient(ctx, cfg)
106107
if err != nil {
107108
return err
108109
}
109-
svc := ses.New(awsSession)
110110
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
111111
emails := strings.Split(cfg.To, ",")
112-
113112
input := &ses.SendEmailInput{
114-
Destination: &ses.Destination{
115-
ToAddresses: aws.StringSlice(emails),
113+
Destination: &types.Destination{
114+
ToAddresses: emails,
116115
},
117-
Message: &ses.Message{
118-
Body: &ses.Body{
119-
Text: &ses.Content{
116+
Message: &types.Message{
117+
Body: &types.Body{
118+
Text: &types.Content{
120119
Charset: aws.String(CharSet),
121120
Data: aws.String(body),
122121
},
123122
},
124-
Subject: &ses.Content{
123+
Subject: &types.Content{
125124
Charset: aws.String(CharSet),
126125
Data: aws.String(subject),
127126
},
128127
},
129128
Source: aws.String(cfg.From),
130129
}
131-
if _, err = svc.SendEmail(input); err != nil {
132-
if aerr, ok := err.(awserr.Error); ok {
133-
switch aerr.Code() {
134-
case ses.ErrCodeMessageRejected:
135-
logr.Error(ses.ErrCodeMessageRejected + ": " + aerr.Error())
136-
case ses.ErrCodeMailFromDomainNotVerifiedException:
137-
logr.Error(ses.ErrCodeMailFromDomainNotVerifiedException + ": " + aerr.Error())
138-
case ses.ErrCodeConfigurationSetDoesNotExistException:
139-
logr.Error(ses.ErrCodeConfigurationSetDoesNotExistException + ": " + aerr.Error())
140-
default:
141-
logr.Error(aerr.Error())
142-
}
143-
} else {
144-
// Print the error, cast err to awserr.Error to get the Code and
145-
// Message from an error.
146-
logr.Error(err.Error())
147-
}
148-
130+
if _, err = svc.SendEmail(ctx, input); err != nil {
149131
return err
150132
}
151133
return nil
152134
}
153135

154-
func (provider *AlertProvider) createSession(cfg *Config) (*session.Session, error) {
155-
awsConfig := &aws.Config{
156-
Region: aws.String(cfg.Region),
136+
func (provider *AlertProvider) createClient(ctx context.Context, cfg *Config) (*ses.Client, error) {
137+
var opts []func(*config.LoadOptions) error
138+
if len(cfg.Region) > 0 {
139+
opts = append(opts, config.WithRegion(cfg.Region))
157140
}
158141
if len(cfg.AccessKeyID) > 0 && len(cfg.SecretAccessKey) > 0 {
159-
awsConfig.Credentials = credentials.NewStaticCredentials(cfg.AccessKeyID, cfg.SecretAccessKey, "")
142+
opts = append(opts, config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretAccessKey, "")))
143+
}
144+
awsConfig, err := config.LoadDefaultConfig(ctx, opts...)
145+
if err != nil {
146+
return nil, err
160147
}
161-
return session.NewSession(awsConfig)
148+
return ses.NewFromConfig(awsConfig), nil
162149
}
163150

164151
// buildMessageSubjectAndBody builds the message subject and body
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package incidentio
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"fmt"
7+
"time"
8+
9+
"github.com/TwiN/gatus/v5/alerting/alert"
10+
"github.com/TwiN/gatus/v5/config/endpoint"
11+
)
12+
13+
// generateDeduplicationKey generates a unique deduplication_key for incident.io
14+
func generateDeduplicationKey(ep *endpoint.Endpoint, alert *alert.Alert) string {
15+
data := fmt.Sprintf("%s|%s|%s|%d", ep.Key(), alert.Type, alert.GetDescription(), time.Now().UnixNano())
16+
hash := sha256.Sum256([]byte(data))
17+
return hex.EncodeToString(hash[:])
18+
}

alerting/provider/incidentio/incidentio.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,27 +153,44 @@ func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoi
153153
} else {
154154
prefix = "🔴"
155155
}
156-
// No need for \n since incident.io trims it anyways.
157156
formattedConditionResults += fmt.Sprintf(" %s %s ", prefix, conditionResult.Condition)
158157
}
159158
if len(alert.GetDescription()) > 0 {
160159
message += " with the following description: " + alert.GetDescription()
161160
}
162161
message += fmt.Sprintf(" and the following conditions: %s ", formattedConditionResults)
163-
var body []byte
162+
163+
// Generate deduplication key if empty (first firing)
164+
if alert.ResolveKey == "" {
165+
// Generate unique key (endpoint key, alert type, timestamp)
166+
alert.ResolveKey = generateDeduplicationKey(ep, alert)
167+
}
168+
// Extract alert_source_config_id from URL
164169
alertSourceID := strings.TrimPrefix(cfg.URL, restAPIUrl)
165-
body, _ = json.Marshal(Body{
170+
// Merge metadata: cfg.Metadata + ep.ExtraLabels (if present)
171+
mergedMetadata := map[string]interface{}{}
172+
// Copy cfg.Metadata
173+
for k, v := range cfg.Metadata {
174+
mergedMetadata[k] = v
175+
}
176+
// Add extra labels from endpoint (if present)
177+
if ep.ExtraLabels != nil && len(ep.ExtraLabels) > 0 {
178+
for k, v := range ep.ExtraLabels {
179+
mergedMetadata[k] = v
180+
}
181+
}
182+
183+
body, _ := json.Marshal(Body{
166184
AlertSourceConfigID: alertSourceID,
167185
Title: "Gatus: " + ep.DisplayName(),
168186
Status: status,
169187
DeduplicationKey: alert.ResolveKey,
170188
Description: message,
171189
SourceURL: cfg.SourceURL,
172-
Metadata: cfg.Metadata,
190+
Metadata: mergedMetadata,
173191
})
174192
fmt.Printf("%v", string(body))
175193
return body
176-
177194
}
178195
func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
179196
cfg := provider.DefaultConfig

0 commit comments

Comments
 (0)