From 35038a63c44a7fdabef8be51a8478e12a021eba4 Mon Sep 17 00:00:00 2001 From: TwiN Date: Tue, 4 Oct 2022 23:26:34 -0400 Subject: [PATCH] feat(alerting): Implement ntfy provider Closes #308 Work remaining: - Add the documentation on the README.md - Test it with an actual Ntfy instance (I've only used https://ntfy.sh/docs/examples/#gatus as a reference; I haven't actually tested it yet) --- alerting/alert/type.go | 3 ++ alerting/config.go | 10 ++++ alerting/provider/ntfy/ntfy.go | 73 +++++++++++++++++++++++++++++ alerting/provider/ntfy/ntfy_test.go | 44 +++++++++++++++++ alerting/provider/provider.go | 2 + config/config.go | 1 + 6 files changed, 133 insertions(+) create mode 100644 alerting/provider/ntfy/ntfy.go create mode 100644 alerting/provider/ntfy/ntfy_test.go diff --git a/alerting/alert/type.go b/alerting/alert/type.go index 78c92fa8..a0469386 100644 --- a/alerting/alert/type.go +++ b/alerting/alert/type.go @@ -26,6 +26,9 @@ const ( // TypeMessagebird is the Type for the messagebird alerting provider TypeMessagebird Type = "messagebird" + // TypeNtfy is the Type for the ntfy alerting provider + TypeNtfy Type = "ntfy" + // TypeOpsgenie is the Type for the opsgenie alerting provider TypeOpsgenie Type = "opsgenie" diff --git a/alerting/config.go b/alerting/config.go index 82e4ba4a..106b2e33 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -10,6 +10,7 @@ import ( "github.com/TwiN/gatus/v4/alerting/provider/matrix" "github.com/TwiN/gatus/v4/alerting/provider/mattermost" "github.com/TwiN/gatus/v4/alerting/provider/messagebird" + "github.com/TwiN/gatus/v4/alerting/provider/ntfy" "github.com/TwiN/gatus/v4/alerting/provider/opsgenie" "github.com/TwiN/gatus/v4/alerting/provider/pagerduty" "github.com/TwiN/gatus/v4/alerting/provider/slack" @@ -41,6 +42,9 @@ type Config struct { // Messagebird is the configuration for the messagebird alerting provider Messagebird *messagebird.AlertProvider `yaml:"messagebird,omitempty"` + // Ntfy is the configuration for the ntfy alerting provider + Ntfy *ntfy.AlertProvider `yaml:"ntfy,omitempty"` + // Opsgenie is the configuration for the opsgenie alerting provider Opsgenie *opsgenie.AlertProvider `yaml:"opsgenie,omitempty"` @@ -105,6 +109,12 @@ func (config Config) GetAlertingProviderByAlertType(alertType alert.Type) provid return nil } return config.Messagebird + case alert.TypeNtfy: + if config.Ntfy == nil { + // Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil + return nil + } + return config.Ntfy case alert.TypeOpsgenie: if config.Opsgenie == nil { // Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil diff --git a/alerting/provider/ntfy/ntfy.go b/alerting/provider/ntfy/ntfy.go new file mode 100644 index 00000000..1c64d303 --- /dev/null +++ b/alerting/provider/ntfy/ntfy.go @@ -0,0 +1,73 @@ +package ntfy + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "github.com/TwiN/gatus/v4/alerting/alert" + "github.com/TwiN/gatus/v4/client" + "github.com/TwiN/gatus/v4/core" +) + +// AlertProvider is the configuration necessary for sending an alert using Slack +type AlertProvider struct { + URL string `yaml:"url"` + Topic string `yaml:"topic"` + Priority int `yaml:"priority"` + + // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type + DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"` +} + +// IsValid returns whether the provider's configuration is valid +func (provider *AlertProvider) IsValid() bool { + return len(provider.URL) > 0 && len(provider.Topic) > 0 && provider.Priority > 0 && provider.Priority < 6 +} + +// 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([]byte(provider.buildRequestBody(endpoint, alert, result, resolved))) + request, err := http.NewRequest(http.MethodPost, provider.URL, 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 + } + if response.StatusCode > 399 { + body, _ := io.ReadAll(response.Body) + return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body)) + } + return err +} + +// buildRequestBody builds the request body for the provider +func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string { + var message, tag string + if len(alert.GetDescription()) > 0 { + message = endpoint.DisplayName() + " - " + alert.GetDescription() + } else { + message = endpoint.DisplayName() + } + if resolved { + tag = "x" + } else { + tag = "white_check_mark" + } + return fmt.Sprintf(`{ + "topic": "%s", + "title": "Gatus", + "message": "%s", + "tags": ["%s"], + "priority": %d +}`, provider.Topic, message, tag, provider.Priority) +} + +// GetDefaultAlert returns the provider's default alert configuration +func (provider AlertProvider) GetDefaultAlert() *alert.Alert { + return provider.DefaultAlert +} diff --git a/alerting/provider/ntfy/ntfy_test.go b/alerting/provider/ntfy/ntfy_test.go new file mode 100644 index 00000000..be0fff95 --- /dev/null +++ b/alerting/provider/ntfy/ntfy_test.go @@ -0,0 +1,44 @@ +package ntfy + +import "testing" + +func TestAlertDefaultProvider_IsValid(t *testing.T) { + scenarios := []struct { + name string + provider AlertProvider + expected bool + }{ + { + name: "valid", + provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1}, + expected: true, + }, + { + name: "invalid-url", + provider: AlertProvider{URL: "", Topic: "example", Priority: 1}, + expected: false, + }, + { + name: "invalid-topic", + provider: AlertProvider{URL: "https://ntfy.sh", Topic: "", Priority: 1}, + expected: false, + }, + { + name: "invalid-priority-too-high", + provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 6}, + expected: false, + }, + { + name: "invalid-priority-too-low", + provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 0}, + expected: false, + }, + } + 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()) + } + }) + } +} diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go index 1f105648..b7afb0d8 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -9,6 +9,7 @@ import ( "github.com/TwiN/gatus/v4/alerting/provider/matrix" "github.com/TwiN/gatus/v4/alerting/provider/mattermost" "github.com/TwiN/gatus/v4/alerting/provider/messagebird" + "github.com/TwiN/gatus/v4/alerting/provider/ntfy" "github.com/TwiN/gatus/v4/alerting/provider/opsgenie" "github.com/TwiN/gatus/v4/alerting/provider/pagerduty" "github.com/TwiN/gatus/v4/alerting/provider/slack" @@ -61,6 +62,7 @@ var ( _ AlertProvider = (*matrix.AlertProvider)(nil) _ AlertProvider = (*mattermost.AlertProvider)(nil) _ AlertProvider = (*messagebird.AlertProvider)(nil) + _ AlertProvider = (*ntfy.AlertProvider)(nil) _ AlertProvider = (*opsgenie.AlertProvider)(nil) _ AlertProvider = (*pagerduty.AlertProvider)(nil) _ AlertProvider = (*slack.AlertProvider)(nil) diff --git a/config/config.go b/config/config.go index d9dc3d01..3dc1078a 100644 --- a/config/config.go +++ b/config/config.go @@ -306,6 +306,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E alert.TypeMatrix, alert.TypeMattermost, alert.TypeMessagebird, + alert.TypeNtfy, alert.TypeOpsgenie, alert.TypePagerDuty, alert.TypeSlack,