diff --git a/.github/assets/gotify-alerts.png b/.github/assets/gotify-alerts.png
new file mode 100644
index 00000000..a36387b8
Binary files /dev/null and b/.github/assets/gotify-alerts.png differ
diff --git a/README.md b/README.md
index 231183d0..aba5fb99 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Configuring GitHub alerts](#configuring-github-alerts)
- [Configuring GitLab alerts](#configuring-gitlab-alerts)
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
+ - [Configuring Gotify alerts](#configuring-gotify-alerts)
- [Configuring Matrix alerts](#configuring-matrix-alerts)
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
@@ -423,6 +424,7 @@ ignored.
| `alerting.github` | Configuration for alerts of type `github`.
See [Configuring GitHub alerts](#configuring-github-alerts). | `{}` |
| `alerting.gitlab` | Configuration for alerts of type `gitlab`.
See [Configuring GitLab alerts](#configuring-gitlab-alerts). | `{}` |
| `alerting.googlechat` | Configuration for alerts of type `googlechat`.
See [Configuring Google Chat alerts](#configuring-google-chat-alerts). | `{}` |
+| `alerting.gotify` | Configuration for alerts of type `gotify`.
See [Configuring Gotify alerts](#configuring-gotify-alerts). | `{}` |
| `alerting.matrix` | Configuration for alerts of type `matrix`.
See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost`.
See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
| `alerting.messagebird` | Configuration for alerts of type `messagebird`.
See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
@@ -638,6 +640,41 @@ endpoints:
```
+#### Configuring Gotify alerts
+| Parameter | Description | Default |
+|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:-----------------------|
+| `alerting.gotify` | Configuration for alerts of type `gotify` | `{}` |
+| `alerting.gotify.server-url` | Gotify server URL | Required `""` |
+| `alerting.gotify.token` | Token that is used for authentication. | Required `""` |
+| `alerting.gotify.priority` | Priority of the alert according to Gotify standarts. | `5` |
+| `alerting.gotify.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert). | N/A |
+| `alerting.gotify.title` | Title of the notification | `"Gatus: "` |
+
+```yaml
+alerting:
+ gotify:
+ server-url: "https://gotify.example"
+ token: "**************"
+
+endpoints:
+ - name: website
+ url: "https://twin.sh/health"
+ interval: 5m
+ conditions:
+ - "[STATUS] == 200"
+ - "[BODY].status == UP"
+ - "[RESPONSE_TIME] < 300"
+ alerts:
+ - type: gotify
+ description: "healthcheck failed"
+ send-on-resolved: true
+```
+
+Here's an example of what the notifications look like:
+
+![Gotify notifications](.github/assets/gotify-alerts.png)
+
+
#### Configuring Matrix alerts
| Parameter | Description | Default |
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:-----------------------------------|
diff --git a/alerting/alert/type.go b/alerting/alert/type.go
index 72e201bc..22f7b8d6 100644
--- a/alerting/alert/type.go
+++ b/alerting/alert/type.go
@@ -26,6 +26,9 @@ const (
// TypeGoogleChat is the Type for the googlechat alerting provider
TypeGoogleChat Type = "googlechat"
+ // TypeGotify is the Type for the gotify alerting provider
+ TypeGotify Type = "gotify"
+
// TypeMatrix is the Type for the matrix alerting provider
TypeMatrix Type = "matrix"
diff --git a/alerting/config.go b/alerting/config.go
index 6b35208d..af52115d 100644
--- a/alerting/config.go
+++ b/alerting/config.go
@@ -14,6 +14,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/github"
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
+ "github.com/TwiN/gatus/v5/alerting/provider/gotify"
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
@@ -50,6 +51,9 @@ type Config struct {
// GoogleChat is the configuration for the googlechat alerting provider
GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"`
+ // Gotify is the configuration for the gotify alerting provider
+ Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"`
+
// Matrix is the configuration for the matrix alerting provider
Matrix *matrix.AlertProvider `yaml:"matrix,omitempty"`
diff --git a/alerting/provider/gotify/gotify.go b/alerting/provider/gotify/gotify.go
new file mode 100644
index 00000000..e9b1d59b
--- /dev/null
+++ b/alerting/provider/gotify/gotify.go
@@ -0,0 +1,105 @@
+package gotify
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/TwiN/gatus/v5/alerting/alert"
+ "github.com/TwiN/gatus/v5/client"
+ "github.com/TwiN/gatus/v5/core"
+)
+
+const DefaultPriority = 5
+
+// AlertProvider is the configuration necessary for sending an alert using Gotify
+type AlertProvider struct {
+ // ServerURL is the URL of the Gotify server
+ ServerURL string `yaml:"server-url"`
+
+ // Token is the token to use when sending a message to the Gotify server
+ Token string `yaml:"token"`
+
+ // Priority is the priority of the message
+ Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+
+ // Title is the title of the message that will be sent
+ Title string `yaml:"title,omitempty"`
+}
+
+// IsValid returns whether the provider's configuration is valid
+func (provider *AlertProvider) IsValid() bool {
+ if provider.Priority == 0 {
+ provider.Priority = DefaultPriority
+ }
+ return len(provider.ServerURL) > 0 && len(provider.Token) > 0
+}
+
+// Send an alert using the provider
+func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
+ buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
+ request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
+ if err != nil {
+ return err
+ }
+ request.Header.Set("Content-Type", "application/json")
+ response, err := client.GetHTTPClient(nil).Do(request)
+ if err != nil {
+ return err
+ }
+ defer response.Body.Close()
+ if response.StatusCode > 399 {
+ body, _ := io.ReadAll(response.Body)
+ return fmt.Errorf("failed to send alert to Gotify: %s", string(body))
+ }
+ return nil
+}
+
+type Body struct {
+ Message string `json:"message"`
+ Title string `json:"title"`
+ Priority int `json:"priority"`
+}
+
+// buildRequestBody builds the request body for the provider
+func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
+ var message, results string
+ if resolved {
+ message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
+ } else {
+ 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("\n%s - %s", prefix, conditionResult.Condition)
+ }
+ if len(alert.GetDescription()) > 0 {
+ message += " with the following description: " + alert.GetDescription()
+ }
+ message += results
+ title := "Gatus: " + endpoint.DisplayName()
+ if provider.Title != "" {
+ title = provider.Title
+ }
+ body, _ := json.Marshal(Body{
+ Message: message,
+ Title: title,
+ Priority: provider.Priority,
+ })
+ return body
+}
+
+// GetDefaultAlert returns the provider's default alert configuration
+func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
+ return provider.DefaultAlert
+}
diff --git a/alerting/provider/gotify/gotify_test.go b/alerting/provider/gotify/gotify_test.go
new file mode 100644
index 00000000..19a68b97
--- /dev/null
+++ b/alerting/provider/gotify/gotify_test.go
@@ -0,0 +1,105 @@
+package gotify
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "github.com/TwiN/gatus/v5/alerting/alert"
+ "github.com/TwiN/gatus/v5/core"
+)
+
+func TestAlertProvider_IsValid(t *testing.T) {
+ scenarios := []struct {
+ name string
+ provider AlertProvider
+ expected bool
+ }{
+ {
+ name: "valid",
+ provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
+ expected: true,
+ },
+ {
+ name: "invalid-server-url",
+ provider: AlertProvider{ServerURL: "", Token: "faketoken"},
+ expected: false,
+ },
+ {
+ name: "invalid-app-token",
+ provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: ""},
+ expected: false,
+ },
+ {
+ name: "no-priority-should-use-default-value",
+ provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
+ expected: true,
+ },
+ }
+ for _, scenario := range scenarios {
+ t.Run(scenario.name, func(t *testing.T) {
+ if scenario.provider.IsValid() != scenario.expected {
+ t.Errorf("expected %t, got %t", scenario.expected, scenario.provider.IsValid())
+ }
+ })
+ }
+}
+
+func TestAlertProvider_buildRequestBody(t *testing.T) {
+ var (
+ description = "custom-description"
+ //title = "custom-title"
+ endpoint = "custom-endpoint"
+ )
+ scenarios := []struct {
+ Name string
+ Provider AlertProvider
+ Alert alert.Alert
+ Resolved bool
+ ExpectedBody string
+ }{
+ {
+ Name: "triggered",
+ Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
+ Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: false,
+ ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
+ },
+ {
+ Name: "resolved",
+ Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
+ Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: true,
+ ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been resolved after passing successfully 5 time(s) in a row with the following description: %s\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
+ },
+ {
+ Name: "custom-title",
+ Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
+ Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: false,
+ ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"custom-title\",\"priority\":0}", endpoint, description),
+ },
+ }
+ for _, scenario := range scenarios {
+ t.Run(scenario.Name, func(t *testing.T) {
+ body := scenario.Provider.buildRequestBody(
+ &core.Endpoint{Name: endpoint},
+ &scenario.Alert,
+ &core.Result{
+ ConditionResults: []*core.ConditionResult{
+ {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
+ {Condition: "[STATUS] == 200", Success: scenario.Resolved},
+ },
+ },
+ scenario.Resolved,
+ )
+ if string(body) != scenario.ExpectedBody {
+ t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
+ }
+ out := make(map[string]interface{})
+ if err := json.Unmarshal(body, &out); err != nil {
+ t.Error("expected body to be valid JSON, got error:", err.Error())
+ }
+ })
+ }
+}
diff --git a/config/config.go b/config/config.go
index d2824ca9..70f62f2c 100644
--- a/config/config.go
+++ b/config/config.go
@@ -367,6 +367,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
alert.TypeGitHub,
alert.TypeGitLab,
alert.TypeGoogleChat,
+ alert.TypeGotify,
alert.TypeEmail,
alert.TypeMatrix,
alert.TypeMattermost,