mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-23 16:33:45 +01:00
feat(alerting): Add Pushover provider (#405)
* Add a new Pushover provider (#129) - Adds new provider named Pushover with corresponding tests - Adds Pushover as a provider to the configuration and adjusts test accordingly - Adds Pushover to alerting_test.go, provider.go and type.go - Updates the readme with configuration details * Correct import order * Fix some missing pushover references * Apply suggestions from code review * Rename application-key to application-token for Pushover --------- Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
parent
d75180c341
commit
21f62f362f
36
README.md
36
README.md
@ -10,7 +10,7 @@
|
|||||||
Gatus is a developer-oriented health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS
|
Gatus is a developer-oriented health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS
|
||||||
queries as well as evaluate the result of said queries by using a list of conditions on values like the status code,
|
queries as well as evaluate the result of said queries by using a list of conditions on values like the status code,
|
||||||
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
|
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
|
||||||
checks can be paired with alerting via Slack, PagerDuty, Discord, Twilio and more.
|
checks can be paired with alerting via Slack, PagerDuty, Pushover, Discord, Twilio and more.
|
||||||
|
|
||||||
I personally deploy it in my Kubernetes cluster and let it monitor the status of my
|
I personally deploy it in my Kubernetes cluster and let it monitor the status of my
|
||||||
core applications: https://status.twin.sh/
|
core applications: https://status.twin.sh/
|
||||||
@ -56,6 +56,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [Configuring Ntfy alerts](#configuring-ntfy-alerts)
|
- [Configuring Ntfy alerts](#configuring-ntfy-alerts)
|
||||||
- [Configuring Opsgenie alerts](#configuring-opsgenie-alerts)
|
- [Configuring Opsgenie alerts](#configuring-opsgenie-alerts)
|
||||||
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
|
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
|
||||||
|
- [Configuring Pushover alerts](#configuring-pushover-alerts)
|
||||||
- [Configuring Slack alerts](#configuring-slack-alerts)
|
- [Configuring Slack alerts](#configuring-slack-alerts)
|
||||||
- [Configuring Teams alerts](#configuring-teams-alerts)
|
- [Configuring Teams alerts](#configuring-teams-alerts)
|
||||||
- [Configuring Telegram alerts](#configuring-telegram-alerts)
|
- [Configuring Telegram alerts](#configuring-telegram-alerts)
|
||||||
@ -411,6 +412,7 @@ ignored.
|
|||||||
| `alerting.ntfy` | Configuration for alerts of type `ntfy`. <br />See [Configuring Ntfy alerts](#configuring-ntfy-alerts). | `{}` |
|
| `alerting.ntfy` | Configuration for alerts of type `ntfy`. <br />See [Configuring Ntfy alerts](#configuring-ntfy-alerts). | `{}` |
|
||||||
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie`. <br />See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` |
|
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie`. <br />See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` |
|
||||||
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. <br />See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
|
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. <br />See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
|
||||||
|
| `alerting.pushover` | Configuration for alerts of type `pushover`. <br />See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` |
|
||||||
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
|
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
|
||||||
| `alerting.teams` | Configuration for alerts of type `teams`. <br />See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` |
|
| `alerting.teams` | Configuration for alerts of type `teams`. <br />See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` |
|
||||||
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
|
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
|
||||||
@ -791,6 +793,38 @@ endpoints:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Configuring Pushover alerts
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:---------------------------------------|:-------------------------------------------------------------------------------------------|:-----------------------------|
|
||||||
|
| `alerting.pushover` | Configuration for alerts of type `pushover` | `{}` |
|
||||||
|
| `alerting.pushover.application-token` | Pushover Application token | `""` |
|
||||||
|
| `alerting.pushover.user-key` | User or group key | `""` |
|
||||||
|
| `alerting.pushover.title` | A fixed title for all messages sent via Pushover | Name of your App in Pushover |
|
||||||
|
| `alerting.pushover.priority` | Priority of all messages, ranging from -2 (very low) to 2 (Emergency) | `0` |
|
||||||
|
| `alerting.pushover.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alerting:
|
||||||
|
pushover:
|
||||||
|
application-token: "******************************"
|
||||||
|
user-key: "******************************"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
interval: 30s
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- "[BODY].status == UP"
|
||||||
|
- "[RESPONSE_TIME] < 300"
|
||||||
|
alerts:
|
||||||
|
- type: pushover
|
||||||
|
failure-threshold: 3
|
||||||
|
success-threshold: 5
|
||||||
|
send-on-resolved: true
|
||||||
|
description: "healthcheck failed"
|
||||||
|
```
|
||||||
|
|
||||||
#### Configuring Slack alerts
|
#### Configuring Slack alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
|:------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
|
@ -38,6 +38,9 @@ const (
|
|||||||
// TypePagerDuty is the Type for the pagerduty alerting provider
|
// TypePagerDuty is the Type for the pagerduty alerting provider
|
||||||
TypePagerDuty Type = "pagerduty"
|
TypePagerDuty Type = "pagerduty"
|
||||||
|
|
||||||
|
// TypePushover is the Type for the pushover alerting provider
|
||||||
|
TypePushover Type = "pushover"
|
||||||
|
|
||||||
// TypeSlack is the Type for the slack alerting provider
|
// TypeSlack is the Type for the slack alerting provider
|
||||||
TypeSlack Type = "slack"
|
TypeSlack Type = "slack"
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
|
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
|
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
|
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
@ -59,6 +60,9 @@ type Config struct {
|
|||||||
// PagerDuty is the configuration for the pagerduty alerting provider
|
// PagerDuty is the configuration for the pagerduty alerting provider
|
||||||
PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty,omitempty"`
|
PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty,omitempty"`
|
||||||
|
|
||||||
|
// Pushover is the configuration for the pushover alerting provider
|
||||||
|
Pushover *pushover.AlertProvider `yaml:"pushover,omitempty"`
|
||||||
|
|
||||||
// Slack is the configuration for the slack alerting provider
|
// Slack is the configuration for the slack alerting provider
|
||||||
Slack *slack.AlertProvider `yaml:"slack,omitempty"`
|
Slack *slack.AlertProvider `yaml:"slack,omitempty"`
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
|
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
|
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
|
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
@ -67,6 +68,7 @@ var (
|
|||||||
_ AlertProvider = (*ntfy.AlertProvider)(nil)
|
_ AlertProvider = (*ntfy.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*opsgenie.AlertProvider)(nil)
|
_ AlertProvider = (*opsgenie.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*pagerduty.AlertProvider)(nil)
|
_ AlertProvider = (*pagerduty.AlertProvider)(nil)
|
||||||
|
_ AlertProvider = (*pushover.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*slack.AlertProvider)(nil)
|
_ AlertProvider = (*slack.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*teams.AlertProvider)(nil)
|
_ AlertProvider = (*teams.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*telegram.AlertProvider)(nil)
|
_ AlertProvider = (*telegram.AlertProvider)(nil)
|
||||||
|
106
alerting/provider/pushover/pushover.go
Normal file
106
alerting/provider/pushover/pushover.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package pushover
|
||||||
|
|
||||||
|
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 (
|
||||||
|
restAPIURL = "https://api.pushover.net/1/messages.json"
|
||||||
|
defaultPriority = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertProvider is the configuration necessary for sending an alert using Pushover
|
||||||
|
type AlertProvider struct {
|
||||||
|
// Key used to authenticate the application sending
|
||||||
|
// See "Your Applications" on the dashboard, or add a new one: https://pushover.net/apps/build
|
||||||
|
ApplicationToken string `yaml:"application-token"`
|
||||||
|
|
||||||
|
// Key of the user or group the messages should be sent to
|
||||||
|
UserKey string `yaml:"user-key"`
|
||||||
|
|
||||||
|
// The title of your message, likely the application name
|
||||||
|
// default: the name of your application in Pushover
|
||||||
|
Title string `yaml:"title,omitempty"`
|
||||||
|
|
||||||
|
// Priority of all messages, ranging from -2 (very low) to 2 (Emergency)
|
||||||
|
// default: 0
|
||||||
|
Priority int `yaml:"priority,omitempty"`
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if provider.Priority == 0 {
|
||||||
|
provider.Priority = defaultPriority
|
||||||
|
}
|
||||||
|
return len(provider.ApplicationToken) == 30 && len(provider.UserKey) == 30 && provider.Priority >= -2 && provider.Priority <= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an alert using the provider
|
||||||
|
// Reference doc for pushover: https://pushover.net/api
|
||||||
|
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, restAPIURL, 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("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Body struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
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 string
|
||||||
|
if resolved {
|
||||||
|
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(Body{
|
||||||
|
Token: provider.ApplicationToken,
|
||||||
|
User: provider.UserKey,
|
||||||
|
Title: provider.Title,
|
||||||
|
Message: message,
|
||||||
|
Priority: provider.priority(),
|
||||||
|
})
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *AlertProvider) priority() int {
|
||||||
|
if provider.Priority == 0 {
|
||||||
|
return defaultPriority
|
||||||
|
}
|
||||||
|
return provider.Priority
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
|
return provider.DefaultAlert
|
||||||
|
}
|
174
alerting/provider/pushover/pushover_test.go
Normal file
174
alerting/provider/pushover/pushover_test.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package pushover
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/core"
|
||||||
|
"github.com/TwiN/gatus/v5/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPushoverAlertProvider_IsValid(t *testing.T) {
|
||||||
|
invalidProvider := AlertProvider{}
|
||||||
|
if invalidProvider.IsValid() {
|
||||||
|
t.Error("provider shouldn't have been valid")
|
||||||
|
}
|
||||||
|
validProvider := AlertProvider{
|
||||||
|
ApplicationToken: "aTokenWithLengthOf30characters",
|
||||||
|
UserKey: "aTokenWithLengthOf30characters",
|
||||||
|
Title: "Gatus Notification",
|
||||||
|
Priority: 1,
|
||||||
|
}
|
||||||
|
if !validProvider.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPushoverAlertProvider_IsInvalid(t *testing.T) {
|
||||||
|
invalidProvider := AlertProvider{
|
||||||
|
ApplicationToken: "aTokenWithLengthOfMoreThan30characters",
|
||||||
|
UserKey: "aTokenWithLengthOfMoreThan30characters",
|
||||||
|
Priority: 5,
|
||||||
|
}
|
||||||
|
if invalidProvider.IsValid() {
|
||||||
|
t.Error("provider should've been invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
|
defer client.InjectHTTPClient(nil)
|
||||||
|
firstDescription := "description-1"
|
||||||
|
secondDescription := "description-2"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Alert alert.Alert
|
||||||
|
Resolved bool
|
||||||
|
MockRoundTripper test.MockRoundTripper
|
||||||
|
ExpectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-error",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-error",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
|
err := scenario.Provider.Send(
|
||||||
|
&core.Endpoint{Name: "endpoint-name"},
|
||||||
|
&scenario.Alert,
|
||||||
|
&core.Result{
|
||||||
|
ConditionResults: []*core.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scenario.Resolved,
|
||||||
|
)
|
||||||
|
if scenario.ExpectedError && err == nil {
|
||||||
|
t.Error("expected error, got none")
|
||||||
|
}
|
||||||
|
if !scenario.ExpectedError && err != nil {
|
||||||
|
t.Error("expected no error, got", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
|
firstDescription := "description-1"
|
||||||
|
secondDescription := "description-2"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Alert alert.Alert
|
||||||
|
Resolved bool
|
||||||
|
ExpectedBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{ApplicationToken: "TokenWithLengthOf30Characters1", UserKey: "TokenWithLengthOf30Characters4"},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters1\",\"user\":\"TokenWithLengthOf30Characters4\",\"message\":\"TRIGGERED: endpoint-name - description-1\",\"priority\":0}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"RESOLVED: endpoint-name - description-2\",\"priority\":2}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
body := scenario.Provider.buildRequestBody(
|
||||||
|
&core.Endpoint{Name: "endpoint-name"},
|
||||||
|
&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([]byte(body), &out); err != nil {
|
||||||
|
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
|
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
|
t.Error("expected default alert to be not nil")
|
||||||
|
}
|
||||||
|
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
|
t.Error("expected default alert to be nil")
|
||||||
|
}
|
||||||
|
}
|
@ -352,6 +352,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||||||
alert.TypeNtfy,
|
alert.TypeNtfy,
|
||||||
alert.TypeOpsgenie,
|
alert.TypeOpsgenie,
|
||||||
alert.TypePagerDuty,
|
alert.TypePagerDuty,
|
||||||
|
alert.TypePushover,
|
||||||
alert.TypeSlack,
|
alert.TypeSlack,
|
||||||
alert.TypeTeams,
|
alert.TypeTeams,
|
||||||
alert.TypeTelegram,
|
alert.TypeTelegram,
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
|
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
|
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
|
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
@ -684,6 +685,9 @@ alerting:
|
|||||||
webhook-url: "http://example.org"
|
webhook-url: "http://example.org"
|
||||||
pagerduty:
|
pagerduty:
|
||||||
integration-key: "00000000000000000000000000000000"
|
integration-key: "00000000000000000000000000000000"
|
||||||
|
pushover:
|
||||||
|
application-token: "000000000000000000000000000000"
|
||||||
|
user-key: "000000000000000000000000000000"
|
||||||
mattermost:
|
mattermost:
|
||||||
webhook-url: "http://example.com"
|
webhook-url: "http://example.com"
|
||||||
client:
|
client:
|
||||||
@ -723,6 +727,7 @@ endpoints:
|
|||||||
failure-threshold: 12
|
failure-threshold: 12
|
||||||
success-threshold: 15
|
success-threshold: 15
|
||||||
- type: teams
|
- type: teams
|
||||||
|
- type: pushover
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
`))
|
`))
|
||||||
@ -749,8 +754,8 @@ endpoints:
|
|||||||
if config.Endpoints[0].Interval != 60*time.Second {
|
if config.Endpoints[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
if len(config.Endpoints[0].Alerts) != 8 {
|
if len(config.Endpoints[0].Alerts) != 9 {
|
||||||
t.Fatal("There should've been 8 alerts configured")
|
t.Fatal("There should've been 9 alerts configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
||||||
@ -850,6 +855,13 @@ endpoints:
|
|||||||
if config.Endpoints[0].Alerts[7].SuccessThreshold != 2 {
|
if config.Endpoints[0].Alerts[7].SuccessThreshold != 2 {
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold)
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Endpoints[0].Alerts[8].Type != alert.TypePushover {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePushover, config.Endpoints[0].Alerts[8].Type)
|
||||||
|
}
|
||||||
|
if !config.Endpoints[0].Alerts[8].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) {
|
||||||
@ -874,6 +886,14 @@ alerting:
|
|||||||
description: default description
|
description: default description
|
||||||
failure-threshold: 7
|
failure-threshold: 7
|
||||||
success-threshold: 5
|
success-threshold: 5
|
||||||
|
pushover:
|
||||||
|
application-token: "000000000000000000000000000000"
|
||||||
|
user-key: "000000000000000000000000000000"
|
||||||
|
default-alert:
|
||||||
|
enabled: true
|
||||||
|
description: default description
|
||||||
|
failure-threshold: 5
|
||||||
|
success-threshold: 3
|
||||||
mattermost:
|
mattermost:
|
||||||
webhook-url: "http://example.com"
|
webhook-url: "http://example.com"
|
||||||
default-alert:
|
default-alert:
|
||||||
@ -917,6 +937,7 @@ endpoints:
|
|||||||
- type: telegram
|
- type: telegram
|
||||||
- type: twilio
|
- type: twilio
|
||||||
- type: teams
|
- type: teams
|
||||||
|
- type: pushover
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
`))
|
`))
|
||||||
@ -953,6 +974,19 @@ endpoints:
|
|||||||
t.Errorf("PagerDuty integration key should've been %s, but was %s", "00000000000000000000000000000000", config.Alerting.PagerDuty.IntegrationKey)
|
t.Errorf("PagerDuty integration key should've been %s, but was %s", "00000000000000000000000000000000", config.Alerting.PagerDuty.IntegrationKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Alerting.Pushover == nil || !config.Alerting.Pushover.IsValid() {
|
||||||
|
t.Fatal("Pushover alerting config should've been valid")
|
||||||
|
}
|
||||||
|
if config.Alerting.Pushover.GetDefaultAlert() == nil {
|
||||||
|
t.Fatal("Pushover.GetDefaultAlert() shouldn't have returned nil")
|
||||||
|
}
|
||||||
|
if config.Alerting.Pushover.ApplicationToken != "000000000000000000000000000000" {
|
||||||
|
t.Errorf("Pushover application token should've been %s, but was %s", "000000000000000000000000000000", config.Alerting.Pushover.ApplicationToken)
|
||||||
|
}
|
||||||
|
if config.Alerting.Pushover.UserKey != "000000000000000000000000000000" {
|
||||||
|
t.Errorf("Pushover user key should've been %s, but was %s", "000000000000000000000000000000", config.Alerting.Pushover.UserKey)
|
||||||
|
}
|
||||||
|
|
||||||
if config.Alerting.Mattermost == nil || !config.Alerting.Mattermost.IsValid() {
|
if config.Alerting.Mattermost == nil || !config.Alerting.Mattermost.IsValid() {
|
||||||
t.Fatal("Mattermost alerting config should've been valid")
|
t.Fatal("Mattermost alerting config should've been valid")
|
||||||
}
|
}
|
||||||
@ -1026,8 +1060,8 @@ endpoints:
|
|||||||
if config.Endpoints[0].Interval != 60*time.Second {
|
if config.Endpoints[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
if len(config.Endpoints[0].Alerts) != 8 {
|
if len(config.Endpoints[0].Alerts) != 9 {
|
||||||
t.Fatal("There should've been 8 alerts configured")
|
t.Fatal("There should've been 9 alerts configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
||||||
@ -1131,6 +1165,19 @@ endpoints:
|
|||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold)
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Endpoints[0].Alerts[8].Type != alert.TypePushover {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePushover, config.Endpoints[0].Alerts[8].Type)
|
||||||
|
}
|
||||||
|
if !config.Endpoints[0].Alerts[8].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[8].FailureThreshold != 5 {
|
||||||
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[8].FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[8].SuccessThreshold != 3 {
|
||||||
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[8].SuccessThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) {
|
||||||
@ -1238,6 +1285,32 @@ endpoints:
|
|||||||
t.Fatal("PagerDuty alerting config should've been set to nil, because its IsValid() method returned false and therefore alerting.Config.SetAlertingProviderToNil() should've been called")
|
t.Fatal("PagerDuty alerting config should've been set to nil, because its IsValid() method returned false and therefore alerting.Config.SetAlertingProviderToNil() should've been called")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func TestParseAndValidateConfigBytesWithInvalidPushoverAlertingConfig(t *testing.T) {
|
||||||
|
config, err := parseAndValidateConfigBytes([]byte(`
|
||||||
|
alerting:
|
||||||
|
pushover:
|
||||||
|
application-token: "INVALID_TOKEN"
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
url: https://twin.sh/health
|
||||||
|
alerts:
|
||||||
|
- type: pushover
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
`))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("expected no error, got", err.Error())
|
||||||
|
}
|
||||||
|
if config == nil {
|
||||||
|
t.Fatal("Config shouldn't have been nil")
|
||||||
|
}
|
||||||
|
if config.Alerting == nil {
|
||||||
|
t.Fatal("config.Alerting shouldn't have been nil")
|
||||||
|
}
|
||||||
|
if config.Alerting.Pushover != nil {
|
||||||
|
t.Fatal("Pushover alerting config should've been set to nil, because its IsValid() method returned false and therefore alerting.Config.SetAlertingProviderToNil() should've been called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithCustomAlertingConfig(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithCustomAlertingConfig(t *testing.T) {
|
||||||
config, err := parseAndValidateConfigBytes([]byte(`
|
config, err := parseAndValidateConfigBytes([]byte(`
|
||||||
@ -1266,7 +1339,7 @@ endpoints:
|
|||||||
t.Fatal("config.Alerting shouldn't have been nil")
|
t.Fatal("config.Alerting shouldn't have been nil")
|
||||||
}
|
}
|
||||||
if config.Alerting.Custom == nil {
|
if config.Alerting.Custom == nil {
|
||||||
t.Fatal("PagerDuty alerting config shouldn't have been nil")
|
t.Fatal("Custom alerting config shouldn't have been nil")
|
||||||
}
|
}
|
||||||
if !config.Alerting.Custom.IsValid() {
|
if !config.Alerting.Custom.IsValid() {
|
||||||
t.Fatal("Custom alerting config should've been valid")
|
t.Fatal("Custom alerting config should've been valid")
|
||||||
@ -1311,7 +1384,7 @@ endpoints:
|
|||||||
t.Fatal("config.Alerting shouldn't have been nil")
|
t.Fatal("config.Alerting shouldn't have been nil")
|
||||||
}
|
}
|
||||||
if config.Alerting.Custom == nil {
|
if config.Alerting.Custom == nil {
|
||||||
t.Fatal("PagerDuty alerting config shouldn't have been nil")
|
t.Fatal("Custom alerting config shouldn't have been nil")
|
||||||
}
|
}
|
||||||
if !config.Alerting.Custom.IsValid() {
|
if !config.Alerting.Custom.IsValid() {
|
||||||
t.Fatal("Custom alerting config should've been valid")
|
t.Fatal("Custom alerting config should've been valid")
|
||||||
@ -1351,7 +1424,7 @@ endpoints:
|
|||||||
t.Fatal("config.Alerting shouldn't have been nil")
|
t.Fatal("config.Alerting shouldn't have been nil")
|
||||||
}
|
}
|
||||||
if config.Alerting.Custom == nil {
|
if config.Alerting.Custom == nil {
|
||||||
t.Fatal("PagerDuty alerting config shouldn't have been nil")
|
t.Fatal("Custom alerting config shouldn't have been nil")
|
||||||
}
|
}
|
||||||
if !config.Alerting.Custom.IsValid() {
|
if !config.Alerting.Custom.IsValid() {
|
||||||
t.Fatal("Custom alerting config should've been valid")
|
t.Fatal("Custom alerting config should've been valid")
|
||||||
@ -1481,6 +1554,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
|
|||||||
Ntfy: &ntfy.AlertProvider{},
|
Ntfy: &ntfy.AlertProvider{},
|
||||||
Opsgenie: &opsgenie.AlertProvider{},
|
Opsgenie: &opsgenie.AlertProvider{},
|
||||||
PagerDuty: &pagerduty.AlertProvider{},
|
PagerDuty: &pagerduty.AlertProvider{},
|
||||||
|
Pushover: &pushover.AlertProvider{},
|
||||||
Slack: &slack.AlertProvider{},
|
Slack: &slack.AlertProvider{},
|
||||||
Telegram: &telegram.AlertProvider{},
|
Telegram: &telegram.AlertProvider{},
|
||||||
Twilio: &twilio.AlertProvider{},
|
Twilio: &twilio.AlertProvider{},
|
||||||
@ -1501,6 +1575,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
|
|||||||
{alertType: alert.TypeNtfy, expected: alertingConfig.Ntfy},
|
{alertType: alert.TypeNtfy, expected: alertingConfig.Ntfy},
|
||||||
{alertType: alert.TypeOpsgenie, expected: alertingConfig.Opsgenie},
|
{alertType: alert.TypeOpsgenie, expected: alertingConfig.Opsgenie},
|
||||||
{alertType: alert.TypePagerDuty, expected: alertingConfig.PagerDuty},
|
{alertType: alert.TypePagerDuty, expected: alertingConfig.PagerDuty},
|
||||||
|
{alertType: alert.TypePushover, expected: alertingConfig.Pushover},
|
||||||
{alertType: alert.TypeSlack, expected: alertingConfig.Slack},
|
{alertType: alert.TypeSlack, expected: alertingConfig.Slack},
|
||||||
{alertType: alert.TypeTelegram, expected: alertingConfig.Telegram},
|
{alertType: alert.TypeTelegram, expected: alertingConfig.Telegram},
|
||||||
{alertType: alert.TypeTwilio, expected: alertingConfig.Twilio},
|
{alertType: alert.TypeTwilio, expected: alertingConfig.Twilio},
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
|
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
@ -203,6 +204,42 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedPagerDuty(t *testing.T) {
|
|||||||
verify(t, endpoint, 0, 1, false, "The alert should've been resolved")
|
verify(t, endpoint, 0, 1, false, "The alert should've been resolved")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleAlertingWhenTriggeredAlertIsResolvedPushover(t *testing.T) {
|
||||||
|
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||||
|
defer os.Clearenv()
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Debug: true,
|
||||||
|
Alerting: &alerting.Config{
|
||||||
|
Pushover: &pushover.AlertProvider{
|
||||||
|
ApplicationToken: "000000000000000000000000000000",
|
||||||
|
UserKey: "000000000000000000000000000000",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
enabled := true
|
||||||
|
endpoint := &core.Endpoint{
|
||||||
|
URL: "https://example.com",
|
||||||
|
Alerts: []*alert.Alert{
|
||||||
|
{
|
||||||
|
Type: alert.TypePushover,
|
||||||
|
Enabled: &enabled,
|
||||||
|
FailureThreshold: 1,
|
||||||
|
SuccessThreshold: 1,
|
||||||
|
SendOnResolved: &enabled,
|
||||||
|
Triggered: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NumberOfFailuresInARow: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleAlerting(endpoint, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||||
|
verify(t, endpoint, 1, 0, true, "")
|
||||||
|
|
||||||
|
HandleAlerting(endpoint, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||||
|
verify(t, endpoint, 0, 1, false, "The alert should've been resolved")
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
|
func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
|
||||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||||
defer os.Clearenv()
|
defer os.Clearenv()
|
||||||
@ -273,6 +310,16 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "pushover",
|
||||||
|
AlertType: alert.TypePushover,
|
||||||
|
AlertingConfig: &alerting.Config{
|
||||||
|
Pushover: &pushover.AlertProvider{
|
||||||
|
ApplicationToken: "000000000000000000000000000000",
|
||||||
|
UserKey: "000000000000000000000000000000",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "slack",
|
Name: "slack",
|
||||||
AlertType: alert.TypeSlack,
|
AlertType: alert.TypeSlack,
|
||||||
|
Loading…
Reference in New Issue
Block a user