mirror of
https://github.com/TwiN/gatus.git
synced 2026-02-15 13:15:05 +00:00
feat(alerting): Add support for n8n alerts (#1309)
This commit is contained in:
50
README.md
50
README.md
@@ -71,6 +71,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [Configuring Matrix alerts](#configuring-matrix-alerts)
|
- [Configuring Matrix alerts](#configuring-matrix-alerts)
|
||||||
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
|
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
|
||||||
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
|
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
|
||||||
|
- [Configuring n8n alerts](#configuring-n8n-alerts)
|
||||||
- [Configuring New Relic alerts](#configuring-new-relic-alerts)
|
- [Configuring New Relic alerts](#configuring-new-relic-alerts)
|
||||||
- [Configuring Ntfy alerts](#configuring-ntfy-alerts)
|
- [Configuring Ntfy alerts](#configuring-ntfy-alerts)
|
||||||
- [Configuring Opsgenie alerts](#configuring-opsgenie-alerts)
|
- [Configuring Opsgenie alerts](#configuring-opsgenie-alerts)
|
||||||
@@ -817,6 +818,7 @@ endpoints:
|
|||||||
| `alerting.matrix` | Configuration for alerts of type `matrix`. <br />See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` |
|
| `alerting.matrix` | Configuration for alerts of type `matrix`. <br />See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` |
|
||||||
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
|
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
|
||||||
| `alerting.messagebird` | Configuration for alerts of type `messagebird`. <br />See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
|
| `alerting.messagebird` | Configuration for alerts of type `messagebird`. <br />See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
|
||||||
|
| `alerting.n8n` | Configuration for alerts of type `n8n`. <br />See [Configuring n8n alerts](#configuring-n8n-alerts). | `{}` |
|
||||||
| `alerting.newrelic` | Configuration for alerts of type `newrelic`. <br />See [Configuring New Relic alerts](#configuring-new-relic-alerts). | `{}` |
|
| `alerting.newrelic` | Configuration for alerts of type `newrelic`. <br />See [Configuring New Relic alerts](#configuring-new-relic-alerts). | `{}` |
|
||||||
| `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). | `{}` |
|
||||||
@@ -1579,8 +1581,8 @@ alerting:
|
|||||||
region: "US" # or "EU" for European region
|
region: "US" # or "EU" for European region
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: example
|
||||||
url: "https://twin.sh/health"
|
url: "https://example.org"
|
||||||
interval: 5m
|
interval: 5m
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
@@ -1590,6 +1592,50 @@ endpoints:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Configuring n8n alerts
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:---------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
|
| `alerting.n8n` | Configuration for alerts of type `n8n` | `{}` |
|
||||||
|
| `alerting.n8n.webhook-url` | n8n webhook URL | Required `""` |
|
||||||
|
| `alerting.n8n.title` | Title of the alert sent to n8n | `""` |
|
||||||
|
| `alerting.n8n.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
|
| `alerting.n8n.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||||
|
| `alerting.n8n.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||||
|
| `alerting.n8n.overrides[].*` | See `alerting.n8n.*` parameters | `{}` |
|
||||||
|
|
||||||
|
[n8n](https://n8n.io/) is a workflow automation platform that allows you to automate tasks across different applications and services using webhooks.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```yaml
|
||||||
|
alerting:
|
||||||
|
n8n:
|
||||||
|
webhook-url: "https://your-n8n-instance.com/webhook/your-webhook-id"
|
||||||
|
title: "Gatus Monitoring"
|
||||||
|
default-alert:
|
||||||
|
send-on-resolved: true
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: example
|
||||||
|
url: "https://example.org"
|
||||||
|
interval: 5m
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
alerts:
|
||||||
|
- type: n8n
|
||||||
|
description: "Health check alert"
|
||||||
|
```
|
||||||
|
|
||||||
|
The JSON payload sent to the n8n webhook will include:
|
||||||
|
- `title`: The configured title
|
||||||
|
- `endpoint_name`: Name of the endpoint
|
||||||
|
- `endpoint_group`: Group of the endpoint (if any)
|
||||||
|
- `endpoint_url`: URL being monitored
|
||||||
|
- `alert_description`: Custom alert description
|
||||||
|
- `resolved`: Boolean indicating if the alert is resolved
|
||||||
|
- `message`: Human-readable alert message
|
||||||
|
- `condition_results`: Array of condition results with their success status
|
||||||
|
|
||||||
|
|
||||||
#### Configuring Ntfy alerts
|
#### Configuring Ntfy alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------|
|
|:-------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------|
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ const (
|
|||||||
// TypeNewRelic is the Type for the newrelic alerting provider
|
// TypeNewRelic is the Type for the newrelic alerting provider
|
||||||
TypeNewRelic Type = "newrelic"
|
TypeNewRelic Type = "newrelic"
|
||||||
|
|
||||||
|
// TypeN8N is the Type for the n8n alerting provider
|
||||||
|
TypeN8N Type = "n8n"
|
||||||
|
|
||||||
// TypeNtfy is the Type for the ntfy alerting provider
|
// TypeNtfy is the Type for the ntfy alerting provider
|
||||||
TypeNtfy Type = "ntfy"
|
TypeNtfy Type = "ntfy"
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
||||||
"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/n8n"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/newrelic"
|
"github.com/TwiN/gatus/v5/alerting/provider/newrelic"
|
||||||
"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"
|
||||||
@@ -66,7 +67,6 @@ type Config struct {
|
|||||||
// Email is the configuration for the email alerting provider
|
// Email is the configuration for the email alerting provider
|
||||||
Email *email.AlertProvider `yaml:"email,omitempty"`
|
Email *email.AlertProvider `yaml:"email,omitempty"`
|
||||||
|
|
||||||
|
|
||||||
// GitHub is the configuration for the github alerting provider
|
// GitHub is the configuration for the github alerting provider
|
||||||
GitHub *github.AlertProvider `yaml:"github,omitempty"`
|
GitHub *github.AlertProvider `yaml:"github,omitempty"`
|
||||||
|
|
||||||
@@ -81,13 +81,13 @@ type Config struct {
|
|||||||
|
|
||||||
// Gotify is the configuration for the gotify alerting provider
|
// Gotify is the configuration for the gotify alerting provider
|
||||||
Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"`
|
Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"`
|
||||||
|
|
||||||
// HomeAssistant is the configuration for the homeassistant alerting provider
|
// HomeAssistant is the configuration for the homeassistant alerting provider
|
||||||
HomeAssistant *homeassistant.AlertProvider `yaml:"homeassistant,omitempty"`
|
HomeAssistant *homeassistant.AlertProvider `yaml:"homeassistant,omitempty"`
|
||||||
|
|
||||||
// IFTTT is the configuration for the ifttt alerting provider
|
// IFTTT is the configuration for the ifttt alerting provider
|
||||||
IFTTT *ifttt.AlertProvider `yaml:"ifttt,omitempty"`
|
IFTTT *ifttt.AlertProvider `yaml:"ifttt,omitempty"`
|
||||||
|
|
||||||
// Ilert is the configuration for the ilert alerting provider
|
// Ilert is the configuration for the ilert alerting provider
|
||||||
Ilert *ilert.AlertProvider `yaml:"ilert,omitempty"`
|
Ilert *ilert.AlertProvider `yaml:"ilert,omitempty"`
|
||||||
|
|
||||||
@@ -112,6 +112,9 @@ type Config struct {
|
|||||||
// NewRelic is the configuration for the newrelic alerting provider
|
// NewRelic is the configuration for the newrelic alerting provider
|
||||||
NewRelic *newrelic.AlertProvider `yaml:"newrelic,omitempty"`
|
NewRelic *newrelic.AlertProvider `yaml:"newrelic,omitempty"`
|
||||||
|
|
||||||
|
// N8N is the configuration for the n8n alerting provider
|
||||||
|
N8N *n8n.AlertProvider `yaml:"n8n,omitempty"`
|
||||||
|
|
||||||
// Ntfy is the configuration for the ntfy alerting provider
|
// Ntfy is the configuration for the ntfy alerting provider
|
||||||
Ntfy *ntfy.AlertProvider `yaml:"ntfy,omitempty"`
|
Ntfy *ntfy.AlertProvider `yaml:"ntfy,omitempty"`
|
||||||
|
|
||||||
|
|||||||
179
alerting/provider/n8n/n8n.go
Normal file
179
alerting/provider/n8n/n8n.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package n8n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrWebhookURLNotSet = errors.New("webhook-url not set")
|
||||||
|
ErrDuplicateGroupOverride = errors.New("duplicate group override")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
WebhookURL string `yaml:"webhook-url"`
|
||||||
|
Title string `yaml:"title,omitempty"` // Title of the message that will be sent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Validate() error {
|
||||||
|
if len(cfg.WebhookURL) == 0 {
|
||||||
|
return ErrWebhookURLNotSet
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Merge(override *Config) {
|
||||||
|
if len(override.WebhookURL) > 0 {
|
||||||
|
cfg.WebhookURL = override.WebhookURL
|
||||||
|
}
|
||||||
|
if len(override.Title) > 0 {
|
||||||
|
cfg.Title = override.Title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertProvider is the configuration necessary for sending an alert using n8n
|
||||||
|
type AlertProvider struct {
|
||||||
|
DefaultConfig Config `yaml:",inline"`
|
||||||
|
// 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"`
|
||||||
|
Config `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the provider's configuration
|
||||||
|
func (provider *AlertProvider) Validate() error {
|
||||||
|
registeredGroups := make(map[string]bool)
|
||||||
|
if provider.Overrides != nil {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
|
||||||
|
return ErrDuplicateGroupOverride
|
||||||
|
}
|
||||||
|
registeredGroups[override.Group] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return provider.DefaultConfig.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an alert using the provider
|
||||||
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
|
cfg, err := provider.GetConfig(ep.Group, alert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
|
||||||
|
request, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, 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 {
|
||||||
|
Title string `json:"title"`
|
||||||
|
EndpointName string `json:"endpoint_name"`
|
||||||
|
EndpointGroup string `json:"endpoint_group,omitempty"`
|
||||||
|
EndpointURL string `json:"endpoint_url"`
|
||||||
|
AlertDescription string `json:"alert_description,omitempty"`
|
||||||
|
Resolved bool `json:"resolved"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
ConditionResults []ConditionResult `json:"condition_results,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConditionResult struct {
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRequestBody builds the request body for the provider
|
||||||
|
func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
|
var message string
|
||||||
|
if resolved {
|
||||||
|
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
|
}
|
||||||
|
title := "Gatus"
|
||||||
|
if cfg.Title != "" {
|
||||||
|
title = cfg.Title
|
||||||
|
}
|
||||||
|
var conditionResults []ConditionResult
|
||||||
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
conditionResults = append(conditionResults, ConditionResult{
|
||||||
|
Condition: conditionResult.Condition,
|
||||||
|
Success: conditionResult.Success,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
body := Body{
|
||||||
|
Title: title,
|
||||||
|
EndpointName: ep.Name,
|
||||||
|
EndpointGroup: ep.Group,
|
||||||
|
EndpointURL: ep.URL,
|
||||||
|
AlertDescription: alert.GetDescription(),
|
||||||
|
Resolved: resolved,
|
||||||
|
Message: message,
|
||||||
|
ConditionResults: conditionResults,
|
||||||
|
}
|
||||||
|
bodyAsJSON, _ := json.Marshal(body)
|
||||||
|
return bodyAsJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
|
return provider.DefaultAlert
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig returns the configuration for the provider with the overrides applied
|
||||||
|
func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
|
||||||
|
cfg := provider.DefaultConfig
|
||||||
|
// Handle group overrides
|
||||||
|
if provider.Overrides != nil {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if group == override.Group {
|
||||||
|
cfg.Merge(&override.Config)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle alert overrides
|
||||||
|
if len(alert.ProviderOverride) != 0 {
|
||||||
|
overrideConfig := Config{}
|
||||||
|
if err := yaml.Unmarshal(alert.ProviderOverrideAsBytes(), &overrideConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.Merge(&overrideConfig)
|
||||||
|
}
|
||||||
|
// Validate the configuration
|
||||||
|
err := cfg.Validate()
|
||||||
|
return &cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateOverrides validates the alert's provider override and, if present, the group override
|
||||||
|
func (provider *AlertProvider) ValidateOverrides(group string, alert *alert.Alert) error {
|
||||||
|
_, err := provider.GetConfig(group, alert)
|
||||||
|
return err
|
||||||
|
}
|
||||||
364
alerting/provider/n8n/n8n_test.go
Normal file
364
alerting/provider/n8n/n8n_test.go
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
package n8n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
"github.com/TwiN/gatus/v5/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlertProvider_Validate(t *testing.T) {
|
||||||
|
invalidProvider := AlertProvider{DefaultConfig: Config{WebhookURL: ""}}
|
||||||
|
if err := invalidProvider.Validate(); err == nil {
|
||||||
|
t.Error("provider shouldn't have been valid")
|
||||||
|
}
|
||||||
|
validProvider := AlertProvider{DefaultConfig: Config{WebhookURL: "https://example.com"}}
|
||||||
|
if err := validProvider.Validate(); err != nil {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_ValidateWithOverride(t *testing.T) {
|
||||||
|
providerWithInvalidOverrideGroup := AlertProvider{
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Config: Config{WebhookURL: "http://example.com"},
|
||||||
|
Group: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := providerWithInvalidOverrideGroup.Validate(); err == nil {
|
||||||
|
t.Error("provider Group shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithInvalidOverrideTo := AlertProvider{
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Config: Config{WebhookURL: ""},
|
||||||
|
Group: "group",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := providerWithInvalidOverrideTo.Validate(); err == nil {
|
||||||
|
t.Error("provider webhook URL shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithValidOverride := AlertProvider{
|
||||||
|
DefaultConfig: Config{WebhookURL: "http://example.com"},
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Config: Config{WebhookURL: "http://example.com"},
|
||||||
|
Group: "group",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := providerWithValidOverride.Validate(); err != nil {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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{DefaultConfig: Config{WebhookURL: "http://example.com"}},
|
||||||
|
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{DefaultConfig: Config{WebhookURL: "http://example.com"}},
|
||||||
|
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{DefaultConfig: Config{WebhookURL: "http://example.com"}},
|
||||||
|
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{DefaultConfig: Config{WebhookURL: "http://example.com"}},
|
||||||
|
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(
|
||||||
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
|
&scenario.Alert,
|
||||||
|
&endpoint.Result{
|
||||||
|
ConditionResults: []*endpoint.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
|
||||||
|
Endpoint endpoint.Endpoint
|
||||||
|
Alert alert.Alert
|
||||||
|
Resolved bool
|
||||||
|
ExpectedBody Body
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com"}},
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "name", URL: "https://example.org"},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: Body{
|
||||||
|
Title: "Gatus",
|
||||||
|
EndpointName: "name",
|
||||||
|
EndpointURL: "https://example.org",
|
||||||
|
AlertDescription: "description-1",
|
||||||
|
Resolved: false,
|
||||||
|
Message: "An alert for name has been triggered due to having failed 3 time(s) in a row",
|
||||||
|
ConditionResults: []ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: false},
|
||||||
|
{Condition: "[STATUS] == 200", Success: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-with-group",
|
||||||
|
Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com"}},
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group", URL: "https://example.org"},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: Body{
|
||||||
|
Title: "Gatus",
|
||||||
|
EndpointName: "name",
|
||||||
|
EndpointGroup: "group",
|
||||||
|
EndpointURL: "https://example.org",
|
||||||
|
AlertDescription: "description-1",
|
||||||
|
Resolved: false,
|
||||||
|
Message: "An alert for group/name has been triggered due to having failed 3 time(s) in a row",
|
||||||
|
ConditionResults: []ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: false},
|
||||||
|
{Condition: "[STATUS] == 200", Success: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com"}},
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "name", URL: "https://example.org"},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: Body{
|
||||||
|
Title: "Gatus",
|
||||||
|
EndpointName: "name",
|
||||||
|
EndpointURL: "https://example.org",
|
||||||
|
AlertDescription: "description-2",
|
||||||
|
Resolved: true,
|
||||||
|
Message: "An alert for name has been resolved after passing successfully 5 time(s) in a row",
|
||||||
|
ConditionResults: []ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: true},
|
||||||
|
{Condition: "[STATUS] == 200", Success: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-with-custom-title",
|
||||||
|
Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com", Title: "Custom Title"}},
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "name", URL: "https://example.org"},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: Body{
|
||||||
|
Title: "Custom Title",
|
||||||
|
EndpointName: "name",
|
||||||
|
EndpointURL: "https://example.org",
|
||||||
|
AlertDescription: "description-2",
|
||||||
|
Resolved: true,
|
||||||
|
Message: "An alert for name has been resolved after passing successfully 5 time(s) in a row",
|
||||||
|
ConditionResults: []ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: true},
|
||||||
|
{Condition: "[STATUS] == 200", Success: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
cfg, err := scenario.Provider.GetConfig(scenario.Endpoint.Group, &scenario.Alert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("couldn't get config:", err.Error())
|
||||||
|
}
|
||||||
|
body := scenario.Provider.buildRequestBody(
|
||||||
|
cfg,
|
||||||
|
&scenario.Endpoint,
|
||||||
|
&scenario.Alert,
|
||||||
|
&endpoint.Result{
|
||||||
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scenario.Resolved,
|
||||||
|
)
|
||||||
|
var actualBody Body
|
||||||
|
if err := json.Unmarshal(body, &actualBody); err != nil {
|
||||||
|
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||||
|
}
|
||||||
|
if actualBody.Title != scenario.ExpectedBody.Title {
|
||||||
|
t.Errorf("expected title to be %s, got %s", scenario.ExpectedBody.Title, actualBody.Title)
|
||||||
|
}
|
||||||
|
if actualBody.EndpointName != scenario.ExpectedBody.EndpointName {
|
||||||
|
t.Errorf("expected endpoint name to be %s, got %s", scenario.ExpectedBody.EndpointName, actualBody.EndpointName)
|
||||||
|
}
|
||||||
|
if actualBody.Resolved != scenario.ExpectedBody.Resolved {
|
||||||
|
t.Errorf("expected resolved to be %v, got %v", scenario.ExpectedBody.Resolved, actualBody.Resolved)
|
||||||
|
}
|
||||||
|
if actualBody.Message != scenario.ExpectedBody.Message {
|
||||||
|
t.Errorf("expected message to be %s, got %s", scenario.ExpectedBody.Message, actualBody.Message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_GetConfig(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
InputGroup string
|
||||||
|
InputAlert alert.Alert
|
||||||
|
ExpectedOutput Config
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
DefaultConfig: Config{WebhookURL: "http://example.com"},
|
||||||
|
Overrides: nil,
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
InputAlert: alert.Alert{},
|
||||||
|
ExpectedOutput: Config{WebhookURL: "http://example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
DefaultConfig: Config{WebhookURL: "http://example.com"},
|
||||||
|
Overrides: nil,
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
InputAlert: alert.Alert{},
|
||||||
|
ExpectedOutput: Config{WebhookURL: "http://example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
DefaultConfig: Config{WebhookURL: "http://example.com"},
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
Config: Config{WebhookURL: "http://example01.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
InputAlert: alert.Alert{},
|
||||||
|
ExpectedOutput: Config{WebhookURL: "http://example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-group-should-override",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
DefaultConfig: Config{WebhookURL: "http://example.com"},
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
Config: Config{WebhookURL: "http://group-example.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
InputAlert: alert.Alert{},
|
||||||
|
ExpectedOutput: Config{WebhookURL: "http://group-example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-group-override-and-alert-override--alert-override-should-take-precedence",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
DefaultConfig: Config{WebhookURL: "http://example.com"},
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
Config: Config{WebhookURL: "http://group-example.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
InputAlert: alert.Alert{ProviderOverride: map[string]any{"webhook-url": "http://alert-example.com"}},
|
||||||
|
ExpectedOutput: Config{WebhookURL: "http://alert-example.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
got, err := scenario.Provider.GetConfig(scenario.InputGroup, &scenario.InputAlert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if got.WebhookURL != scenario.ExpectedOutput.WebhookURL {
|
||||||
|
t.Errorf("expected webhook URL to be %s, got %s", scenario.ExpectedOutput.WebhookURL, got.WebhookURL)
|
||||||
|
}
|
||||||
|
// Test ValidateOverrides as well, since it really just calls GetConfig
|
||||||
|
if err = scenario.Provider.ValidateOverrides(scenario.InputGroup, &scenario.InputAlert); err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
||||||
"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/n8n"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/newrelic"
|
"github.com/TwiN/gatus/v5/alerting/provider/newrelic"
|
||||||
"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"
|
||||||
@@ -110,6 +111,7 @@ var (
|
|||||||
_ AlertProvider = (*matrix.AlertProvider)(nil)
|
_ AlertProvider = (*matrix.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*mattermost.AlertProvider)(nil)
|
_ AlertProvider = (*mattermost.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*messagebird.AlertProvider)(nil)
|
_ AlertProvider = (*messagebird.AlertProvider)(nil)
|
||||||
|
_ AlertProvider = (*n8n.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*newrelic.AlertProvider)(nil)
|
_ AlertProvider = (*newrelic.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*ntfy.AlertProvider)(nil)
|
_ AlertProvider = (*ntfy.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*opsgenie.AlertProvider)(nil)
|
_ AlertProvider = (*opsgenie.AlertProvider)(nil)
|
||||||
@@ -151,6 +153,7 @@ var (
|
|||||||
_ Config[matrix.Config] = (*matrix.Config)(nil)
|
_ Config[matrix.Config] = (*matrix.Config)(nil)
|
||||||
_ Config[mattermost.Config] = (*mattermost.Config)(nil)
|
_ Config[mattermost.Config] = (*mattermost.Config)(nil)
|
||||||
_ Config[messagebird.Config] = (*messagebird.Config)(nil)
|
_ Config[messagebird.Config] = (*messagebird.Config)(nil)
|
||||||
|
_ Config[n8n.Config] = (*n8n.Config)(nil)
|
||||||
_ Config[newrelic.Config] = (*newrelic.Config)(nil)
|
_ Config[newrelic.Config] = (*newrelic.Config)(nil)
|
||||||
_ Config[ntfy.Config] = (*ntfy.Config)(nil)
|
_ Config[ntfy.Config] = (*ntfy.Config)(nil)
|
||||||
_ Config[opsgenie.Config] = (*opsgenie.Config)(nil)
|
_ Config[opsgenie.Config] = (*opsgenie.Config)(nil)
|
||||||
|
|||||||
@@ -612,6 +612,7 @@ func ValidateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi
|
|||||||
alert.TypeMatrix,
|
alert.TypeMatrix,
|
||||||
alert.TypeMattermost,
|
alert.TypeMattermost,
|
||||||
alert.TypeMessagebird,
|
alert.TypeMessagebird,
|
||||||
|
alert.TypeN8N,
|
||||||
alert.TypeNewRelic,
|
alert.TypeNewRelic,
|
||||||
alert.TypeNtfy,
|
alert.TypeNtfy,
|
||||||
alert.TypeOpsgenie,
|
alert.TypeOpsgenie,
|
||||||
|
|||||||
Reference in New Issue
Block a user