Skip to content

Commit bb97397

Browse files
authored
feat(alerting): add email and click action to ntfy provider (#778)
* feat(alerting): add optional email to ntfy provider https://docs.ntfy.sh/publish/#e-mail-notifications * feat(alerting): add optional click action to ntfy provider https://docs.ntfy.sh/publish/#click-action * feat(alerting): add option to disable firebase in ntfy provider https://docs.ntfy.sh/publish/#disable-firebase * feat(alerting): add option to disable message caching in ntfy provider https://docs.ntfy.sh/publish/#message-caching * test(alerting): add buildRequestBody tests for email and click properties * test(alerting): add tests for Send to verify request headers * feat(alerting): refactor to prefix firebase & cache with "disable" This avoids the need for a pointer, as omitting these bools in the config defaults to false and omitting to set these headers will use the server's default - which is enabled on ntfy.sh
1 parent 3a7be5c commit bb97397

File tree

3 files changed

+143
-12
lines changed

3 files changed

+143
-12
lines changed

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -983,14 +983,18 @@ endpoints:
983983

984984

985985
#### Configuring Ntfy alerts
986-
| Parameter | Description | Default |
987-
|:------------------------------|:-------------------------------------------------------------------------------------------|:------------------|
988-
| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` |
989-
| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` |
990-
| `alerting.ntfy.url` | The URL of the target server | `https://ntfy.sh` |
991-
| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` |
992-
| `alerting.ntfy.priority` | The priority of the alert | `3` |
993-
| `alerting.ntfy.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
986+
| Parameter | Description | Default |
987+
|:---------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------|
988+
| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` |
989+
| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` |
990+
| `alerting.ntfy.url` | The URL of the target server | `https://ntfy.sh` |
991+
| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` |
992+
| `alerting.ntfy.email` | E-mail address for additional e-mail notifications | `""` |
993+
| `alerting.ntfy.click` | Website opened when notification is clicked | `""` |
994+
| `alerting.ntfy.priority` | The priority of the alert | `3` |
995+
| `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` |
996+
| `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` |
997+
| `alerting.ntfy.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
994998

995999
[ntfy](https://github.com/binwiederhier/ntfy) is an amazing project that allows you to subscribe to desktop
9961000
and mobile notifications, making it an awesome addition to Gatus.

alerting/provider/ntfy/ntfy.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ const (
2121

2222
// AlertProvider is the configuration necessary for sending an alert using Slack
2323
type AlertProvider struct {
24-
Topic string `yaml:"topic"`
25-
URL string `yaml:"url,omitempty"` // Defaults to DefaultURL
26-
Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
27-
Token string `yaml:"token,omitempty"` // Defaults to ""
24+
Topic string `yaml:"topic"`
25+
URL string `yaml:"url,omitempty"` // Defaults to DefaultURL
26+
Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
27+
Token string `yaml:"token,omitempty"` // Defaults to ""
28+
Email string `yaml:"email,omitempty"` // Defaults to ""
29+
Click string `yaml:"click,omitempty"` // Defaults to ""
30+
DisableFirebase bool `yaml:"disable-firebase,omitempty"` // Defaults to false
31+
DisableCache bool `yaml:"disable-cache,omitempty"` // Defaults to false
2832

2933
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
3034
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -56,6 +60,12 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
5660
if len(provider.Token) > 0 {
5761
request.Header.Set("Authorization", "Bearer "+provider.Token)
5862
}
63+
if provider.DisableFirebase {
64+
request.Header.Set("Firebase", "no")
65+
}
66+
if provider.DisableCache {
67+
request.Header.Set("Cache", "no")
68+
}
5969
response, err := client.GetHTTPClient(nil).Do(request)
6070
if err != nil {
6171
return err
@@ -74,6 +84,8 @@ type Body struct {
7484
Message string `json:"message"`
7585
Tags []string `json:"tags"`
7686
Priority int `json:"priority"`
87+
Email string `json:"email,omitempty"`
88+
Click string `json:"click,omitempty"`
7789
}
7890

7991
// buildRequestBody builds the request body for the provider
@@ -105,6 +117,8 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
105117
Message: message,
106118
Tags: []string{tag},
107119
Priority: provider.Priority,
120+
Email: provider.Email,
121+
Click: provider.Click,
108122
})
109123
return body
110124
}

alerting/provider/ntfy/ntfy_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package ntfy
22

33
import (
44
"encoding/json"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
58
"testing"
69

710
"github.com/TwiN/gatus/v5/alerting/alert"
@@ -88,6 +91,20 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
8891
Resolved: true,
8992
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}`,
9093
},
94+
{
95+
Name: "triggered-email",
96+
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "[email protected]", Click: "example.com"},
97+
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
98+
Resolved: false,
99+
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":"[email protected]","click":"example.com"}`,
100+
},
101+
{
102+
Name: "resolved-email",
103+
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2, Email: "[email protected]", Click: "example.com"},
104+
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
105+
Resolved: true,
106+
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":"[email protected]","click":"example.com"}`,
107+
},
91108
}
92109
for _, scenario := range scenarios {
93110
t.Run(scenario.Name, func(t *testing.T) {
@@ -112,3 +129,99 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
112129
})
113130
}
114131
}
132+
133+
func TestAlertProvider_Send(t *testing.T) {
134+
description := "description-1"
135+
scenarios := []struct {
136+
Name string
137+
Provider AlertProvider
138+
Alert alert.Alert
139+
Resolved bool
140+
ExpectedBody string
141+
ExpectedHeaders map[string]string
142+
}{
143+
{
144+
Name: "triggered",
145+
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "[email protected]", Click: "example.com"},
146+
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
147+
Resolved: false,
148+
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":"[email protected]","click":"example.com"}`,
149+
ExpectedHeaders: map[string]string{
150+
"Content-Type": "application/json",
151+
},
152+
},
153+
{
154+
Name: "no firebase",
155+
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "[email protected]", Click: "example.com", DisableFirebase: true},
156+
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
157+
Resolved: false,
158+
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":"[email protected]","click":"example.com"}`,
159+
ExpectedHeaders: map[string]string{
160+
"Content-Type": "application/json",
161+
"Firebase": "no",
162+
},
163+
},
164+
{
165+
Name: "no cache",
166+
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "[email protected]", Click: "example.com", DisableCache: true},
167+
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
168+
Resolved: false,
169+
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":"[email protected]","click":"example.com"}`,
170+
ExpectedHeaders: map[string]string{
171+
"Content-Type": "application/json",
172+
"Cache": "no",
173+
},
174+
},
175+
{
176+
Name: "neither firebase & cache",
177+
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "[email protected]", Click: "example.com", DisableFirebase: true, DisableCache: true},
178+
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
179+
Resolved: false,
180+
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":"[email protected]","click":"example.com"}`,
181+
ExpectedHeaders: map[string]string{
182+
"Content-Type": "application/json",
183+
"Firebase": "no",
184+
"Cache": "no",
185+
},
186+
},
187+
}
188+
for _, scenario := range scenarios {
189+
t.Run(scenario.Name, func(t *testing.T) {
190+
// Start a local HTTP server
191+
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
192+
// Test request parameters
193+
for header, value := range scenario.ExpectedHeaders {
194+
if value != req.Header.Get(header) {
195+
t.Errorf("expected: %s, got: %s", value, req.Header.Get(header))
196+
}
197+
}
198+
body, _ := io.ReadAll(req.Body)
199+
if string(body) != scenario.ExpectedBody {
200+
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
201+
}
202+
// Send response to be tested
203+
rw.Write([]byte(`OK`))
204+
}))
205+
// Close the server when test finishes
206+
defer server.Close()
207+
208+
scenario.Provider.URL = server.URL
209+
err := scenario.Provider.Send(
210+
&endpoint.Endpoint{Name: "endpoint-name"},
211+
&scenario.Alert,
212+
&endpoint.Result{
213+
ConditionResults: []*endpoint.ConditionResult{
214+
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
215+
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
216+
},
217+
},
218+
scenario.Resolved,
219+
)
220+
if err != nil {
221+
t.Error("Encountered an error on Send: ", err)
222+
}
223+
224+
})
225+
}
226+
227+
}

0 commit comments

Comments
 (0)