1
0
Fork 0
mirror of https://github.com/TwiN/gatus.git synced 2024-12-14 11:58:04 +00:00

feat(alerting): Add overrides for Ntfy provider (#918)

* Add overrides to Ntfy alert provider

* Update alerting/provider/ntfy/ntfy.go

---------

Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
Stephen 2024-11-30 19:09:20 -08:00 committed by GitHub
parent 29cbff6774
commit 778019590d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 206 additions and 24 deletions

View file

@ -988,7 +988,7 @@ endpoints:
#### Configuring Ntfy alerts
| Parameter | Description | Default |
|:---------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------|
|:-------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------|
| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` |
| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` |
| `alerting.ntfy.url` | The URL of the target server | `https://ntfy.sh` |
@ -999,6 +999,14 @@ endpoints:
| `alerting.ntfy.disable-firebase` | Whether message push delivery via firebase should be disabled. [ntfy.sh defaults to enabled](https://docs.ntfy.sh/publish/#disable-firebase) | `false` |
| `alerting.ntfy.disable-cache` | Whether server side message caching should be disabled. [ntfy.sh defaults to enabled](https://docs.ntfy.sh/publish/#message-caching) | `false` |
| `alerting.ntfy.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.ntfy.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.ntfy.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.ntfy.overrides[].topic` | Topic at which the alert will be sent | `""` |
| `alerting.ntfy.overrides[].url` | The URL of the target server | `""` |
| `alerting.ntfy.overrides[].priority` | The priority of the alert | `0` |
| `alerting.ntfy.overrides[].token` | Access token for restricted topics | `""` |
| `alerting.ntfy.overrides[].email` | E-mail address for additional e-mail notifications | `""` |
| `alerting.ntfy.overrides[].click` | Website opened when notification is clicked | `""` |
[ntfy](https://github.com/binwiederhier/ntfy) is an amazing project that allows you to subscribe to desktop
and mobile notifications, making it an awesome addition to Gatus.
@ -1013,6 +1021,13 @@ alerting:
default-alert:
failure-threshold: 3
send-on-resolved: true
# You can also add group-specific to keys, which will
# override the to key above for the specified groups
overrides:
- group: "other"
topic: "gatus-other-test-topic"
priority: 4
click: "https://example.com"
endpoints:
- name: website
@ -1024,6 +1039,16 @@ endpoints:
- "[RESPONSE_TIME] < 300"
alerts:
- type: ntfy
- name: other example
group: other
interval: 30m
url: "https://example.com"
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
alerts:
- type: ntfy
description: example
```

View file

@ -17,6 +17,7 @@ import (
const (
DefaultURL = "https://ntfy.sh"
DefaultPriority = 3
TokenPrefix = "tk_"
)
// AlertProvider is the configuration necessary for sending an alert using Slack
@ -32,6 +33,20 @@ type AlertProvider struct {
// 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"`
Topic string `yaml:"topic"`
URL string `yaml:"url"`
Priority int `yaml:"priority"`
Token string `yaml:"token"`
Email string `yaml:"email"`
Click string `yaml:"click"`
}
// IsValid returns whether the provider's configuration is valid
@ -44,21 +59,42 @@ func (provider *AlertProvider) IsValid() bool {
}
isTokenValid := true
if len(provider.Token) > 0 {
isTokenValid = strings.HasPrefix(provider.Token, "tk_")
isTokenValid = strings.HasPrefix(provider.Token, TokenPrefix)
}
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if len(override.Group) == 0 {
return false
}
if _, ok := registeredGroups[override.Group]; ok {
return false
}
if len(override.Token) > 0 && !strings.HasPrefix(override.Token, TokenPrefix) {
return false
}
if override.Priority < 0 || override.Priority >= 6 {
return false
}
registeredGroups[override.Group] = true
}
}
return len(provider.URL) > 0 && len(provider.Topic) > 0 && provider.Priority > 0 && provider.Priority < 6 && isTokenValid
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
override := provider.getGroupOverride(ep.Group)
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved, override))
url := provider.getURL(override)
request, err := http.NewRequest(http.MethodPost, url, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
if len(provider.Token) > 0 {
request.Header.Set("Authorization", "Bearer "+provider.Token)
if token := provider.getToken(override); len(token) > 0 {
request.Header.Set("Authorization", "Bearer "+token)
}
if provider.DisableFirebase {
request.Header.Set("Firebase", "no")
@ -89,7 +125,7 @@ type Body struct {
}
// buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool, override *Override) []byte {
var message, formattedConditionResults, tag string
if resolved {
tag = "white_check_mark"
@ -112,13 +148,13 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
}
message += formattedConditionResults
body, _ := json.Marshal(Body{
Topic: provider.Topic,
Topic: provider.getTopic(override),
Title: "Gatus: " + ep.DisplayName(),
Message: message,
Tags: []string{tag},
Priority: provider.Priority,
Email: provider.Email,
Click: provider.Click,
Priority: provider.getPriority(override),
Email: provider.getEmail(override),
Click: provider.getClick(override),
})
return body
}
@ -127,3 +163,56 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
func (provider *AlertProvider) getGroupOverride(group string) *Override {
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if group == override.Group {
return &override
}
}
}
return nil
}
func (provider *AlertProvider) getTopic(override *Override) string {
if override != nil && len(override.Topic) > 0 {
return override.Topic
}
return provider.Topic
}
func (provider *AlertProvider) getURL(override *Override) string {
if override != nil && len(override.URL) > 0 {
return override.URL
}
return provider.URL
}
func (provider *AlertProvider) getPriority(override *Override) int {
if override != nil && override.Priority > 0 {
return override.Priority
}
return provider.Priority
}
func (provider *AlertProvider) getToken(override *Override) string {
if override != nil && len(override.Token) > 0 {
return override.Token
}
return provider.Token
}
func (provider *AlertProvider) getEmail(override *Override) string {
if override != nil && len(override.Email) > 0 {
return override.Email
}
return provider.Email
}
func (provider *AlertProvider) getClick(override *Override) string {
if override != nil && len(override.Click) > 0 {
return override.Click
}
return provider.Click
}

View file

@ -57,6 +57,31 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example"},
expected: true,
},
{
name: "invalid-override-token",
provider: AlertProvider{Topic: "example", Overrides: []Override{Override{Group: "g", Token: "xx_faketoken"}}},
expected: false,
},
{
name: "invalid-override-priority",
provider: AlertProvider{Topic: "example", Overrides: []Override{Override{Group: "g", Priority: 8}}},
expected: false,
},
{
name: "no-override-group-name",
provider: AlertProvider{Topic: "example", Overrides: []Override{Override{}}},
expected: false,
},
{
name: "duplicate-override-group-names",
provider: AlertProvider{Topic: "example", Overrides: []Override{Override{Group: "g"}, Override{Group: "g"}}},
expected: false,
},
{
name: "valid-override",
provider: AlertProvider{Topic: "example", Overrides: []Override{Override{Group: "g1", Priority: 4, Click: "https://example.com"}, Override{Group: "g2", Topic: "Example", Token: "tk_faketoken"}}},
expected: true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
@ -75,6 +100,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider AlertProvider
Alert alert.Alert
Resolved bool
Override *Override
ExpectedBody string
}{
{
@ -82,6 +108,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
Override: nil,
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1}`,
},
{
@ -89,6 +116,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
Override: nil,
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2}`,
},
{
@ -96,6 +124,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
Override: nil,
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
},
{
@ -103,8 +132,17 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2, Email: "test@example.com", Click: "example.com"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
Override: nil,
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2,"email":"test@example.com","click":"example.com"}`,
},
{
Name: "override",
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 5, Email: "test@example.com", Click: "example.com"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
Override: &Override{Group: "g", Topic: "override-topic", Priority: 4, Email: "override@test.com", Click: "test.com"},
ExpectedBody: `{"topic":"override-topic","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":4,"email":"override@test.com","click":"test.com"}`,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
@ -118,6 +156,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
},
},
scenario.Resolved,
scenario.Override,
)
if string(body) != scenario.ExpectedBody {
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
@ -137,6 +176,7 @@ func TestAlertProvider_Send(t *testing.T) {
Provider AlertProvider
Alert alert.Alert
Resolved bool
Group string
ExpectedBody string
ExpectedHeaders map[string]string
}{
@ -145,16 +185,30 @@ func TestAlertProvider_Send(t *testing.T) {
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com"},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
Group: "",
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
ExpectedHeaders: map[string]string{
"Content-Type": "application/json",
},
},
{
Name: "token",
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", Token: "tk_mytoken"},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
Group: "",
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
ExpectedHeaders: map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer tk_mytoken",
},
},
{
Name: "no firebase",
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableFirebase: true},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
Group: "",
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
ExpectedHeaders: map[string]string{
"Content-Type": "application/json",
@ -166,6 +220,7 @@ func TestAlertProvider_Send(t *testing.T) {
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableCache: true},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
Group: "",
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
ExpectedHeaders: map[string]string{
"Content-Type": "application/json",
@ -177,6 +232,7 @@ func TestAlertProvider_Send(t *testing.T) {
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableFirebase: true, DisableCache: true},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
Group: "",
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
ExpectedHeaders: map[string]string{
"Content-Type": "application/json",
@ -184,6 +240,18 @@ func TestAlertProvider_Send(t *testing.T) {
"Cache": "no",
},
},
{
Name: "overrides",
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", Token: "tk_mytoken", Overrides: []Override{Override{Group: "other-group", URL: "https://example.com", Token: "tk_othertoken"}, Override{Group: "test-group", Token: "tk_test_token"}}},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
Group: "test-group",
ExpectedBody: `{"topic":"example","title":"Gatus: test-group/endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
ExpectedHeaders: map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer tk_test_token",
},
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
@ -207,7 +275,7 @@ func TestAlertProvider_Send(t *testing.T) {
scenario.Provider.URL = server.URL
err := scenario.Provider.Send(
&endpoint.Endpoint{Name: "endpoint-name"},
&endpoint.Endpoint{Name: "endpoint-name", Group: scenario.Group},
&scenario.Alert,
&endpoint.Result{
ConditionResults: []*endpoint.ConditionResult{