diff --git a/README.md b/README.md
index 50dc8165..677e6ddd 100644
--- a/README.md
+++ b/README.md
@@ -240,7 +240,7 @@ If you want to test it locally, see [Docker](#docker).
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
-If you want more verbose logging, you may set the `GATUS_LOG_LEVEL` environment variable to `DEBUG`.
+If you want more verbose logging, you may set the `GATUS_LOG_LEVEL` environment variable to `DEBUG`.
Conversely, if you want less verbose logging, you can set the aforementioned environment variable to `WARN`, `ERROR` or `FATAL`.
The default value for `GATUS_LOG_LEVEL` is `INFO`.
@@ -987,18 +987,26 @@ 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` |
-| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` |
-| `alerting.ntfy.email` | E-mail address for additional e-mail notifications | `""` |
-| `alerting.ntfy.click` | Website opened when notification is clicked | `""` |
-| `alerting.ntfy.priority` | The priority of the alert | `3` |
-| `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.
See [Setting a default alert](#setting-a-default-alert) | N/A |
+| 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` |
+| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` |
+| `alerting.ntfy.email` | E-mail address for additional e-mail notifications | `""` |
+| `alerting.ntfy.click` | Website opened when notification is clicked | `""` |
+| `alerting.ntfy.priority` | The priority of the alert | `3` |
+| `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.
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
```
diff --git a/alerting/provider/ntfy/ntfy.go b/alerting/provider/ntfy/ntfy.go
index 784caf39..6deba1aa 100644
--- a/alerting/provider/ntfy/ntfy.go
+++ b/alerting/provider/ntfy/ntfy.go
@@ -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
+}
diff --git a/alerting/provider/ntfy/ntfy_test.go b/alerting/provider/ntfy/ntfy_test.go
index 452533f4..304240d8 100644
--- a/alerting/provider/ntfy/ntfy_test.go
+++ b/alerting/provider/ntfy/ntfy_test.go
@@ -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{