From 8853140cb296528b1b614e5c6a965079c51ea4eb Mon Sep 17 00:00:00 2001 From: TwiN Date: Fri, 3 Oct 2025 16:51:26 -0400 Subject: [PATCH] feat(alerting): Add support for n8n alerts (#1309) --- README.md | 50 +++- alerting/alert/type.go | 3 + alerting/config.go | 9 +- alerting/provider/n8n/n8n.go | 179 +++++++++++++++ alerting/provider/n8n/n8n_test.go | 364 ++++++++++++++++++++++++++++++ alerting/provider/provider.go | 3 + config/config.go | 1 + 7 files changed, 604 insertions(+), 5 deletions(-) create mode 100644 alerting/provider/n8n/n8n.go create mode 100644 alerting/provider/n8n/n8n_test.go diff --git a/README.md b/README.md index d584dd92..4321e9ee 100644 --- a/README.md +++ b/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 Mattermost alerts](#configuring-mattermost-alerts) - [Configuring Messagebird alerts](#configuring-messagebird-alerts) + - [Configuring n8n alerts](#configuring-n8n-alerts) - [Configuring New Relic alerts](#configuring-new-relic-alerts) - [Configuring Ntfy alerts](#configuring-ntfy-alerts) - [Configuring Opsgenie alerts](#configuring-opsgenie-alerts) @@ -817,6 +818,7 @@ endpoints: | `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). | `{}` | +| `alerting.n8n` | Configuration for alerts of type `n8n`.
See [Configuring n8n alerts](#configuring-n8n-alerts). | `{}` | | `alerting.newrelic` | Configuration for alerts of type `newrelic`.
See [Configuring New Relic alerts](#configuring-new-relic-alerts). | `{}` | | `alerting.ntfy` | Configuration for alerts of type `ntfy`.
See [Configuring Ntfy alerts](#configuring-ntfy-alerts). | `{}` | | `alerting.opsgenie` | Configuration for alerts of type `opsgenie`.
See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` | @@ -1579,8 +1581,8 @@ alerting: region: "US" # or "EU" for European region endpoints: - - name: website - url: "https://twin.sh/health" + - name: example + url: "https://example.org" interval: 5m conditions: - "[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.
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 | Parameter | Description | Default | |:-------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------| diff --git a/alerting/alert/type.go b/alerting/alert/type.go index f51672b8..334f7d0b 100644 --- a/alerting/alert/type.go +++ b/alerting/alert/type.go @@ -65,6 +65,9 @@ const ( // TypeNewRelic is the Type for the newrelic alerting provider 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 Type = "ntfy" diff --git a/alerting/config.go b/alerting/config.go index 091ef856..c5e39367 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -25,6 +25,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/mattermost" "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/ntfy" "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 *email.AlertProvider `yaml:"email,omitempty"` - // GitHub is the configuration for the github alerting provider GitHub *github.AlertProvider `yaml:"github,omitempty"` @@ -81,13 +81,13 @@ type Config struct { // Gotify is the configuration for the gotify alerting provider Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"` - + // HomeAssistant is the configuration for the homeassistant alerting provider HomeAssistant *homeassistant.AlertProvider `yaml:"homeassistant,omitempty"` // IFTTT is the configuration for the ifttt alerting provider IFTTT *ifttt.AlertProvider `yaml:"ifttt,omitempty"` - + // Ilert is the configuration for the ilert alerting provider Ilert *ilert.AlertProvider `yaml:"ilert,omitempty"` @@ -112,6 +112,9 @@ type Config struct { // NewRelic is the configuration for the newrelic alerting provider 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 *ntfy.AlertProvider `yaml:"ntfy,omitempty"` diff --git a/alerting/provider/n8n/n8n.go b/alerting/provider/n8n/n8n.go new file mode 100644 index 00000000..940442ae --- /dev/null +++ b/alerting/provider/n8n/n8n.go @@ -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 +} diff --git a/alerting/provider/n8n/n8n_test.go b/alerting/provider/n8n/n8n_test.go new file mode 100644 index 00000000..62b5ea13 --- /dev/null +++ b/alerting/provider/n8n/n8n_test.go @@ -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) + } + }) + } +} diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go index 066385f5..b3b883c4 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -21,6 +21,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/mattermost" "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/ntfy" "github.com/TwiN/gatus/v5/alerting/provider/opsgenie" @@ -110,6 +111,7 @@ var ( _ AlertProvider = (*matrix.AlertProvider)(nil) _ AlertProvider = (*mattermost.AlertProvider)(nil) _ AlertProvider = (*messagebird.AlertProvider)(nil) + _ AlertProvider = (*n8n.AlertProvider)(nil) _ AlertProvider = (*newrelic.AlertProvider)(nil) _ AlertProvider = (*ntfy.AlertProvider)(nil) _ AlertProvider = (*opsgenie.AlertProvider)(nil) @@ -151,6 +153,7 @@ var ( _ Config[matrix.Config] = (*matrix.Config)(nil) _ Config[mattermost.Config] = (*mattermost.Config)(nil) _ Config[messagebird.Config] = (*messagebird.Config)(nil) + _ Config[n8n.Config] = (*n8n.Config)(nil) _ Config[newrelic.Config] = (*newrelic.Config)(nil) _ Config[ntfy.Config] = (*ntfy.Config)(nil) _ Config[opsgenie.Config] = (*opsgenie.Config)(nil) diff --git a/config/config.go b/config/config.go index b11362b9..21dd91f0 100644 --- a/config/config.go +++ b/config/config.go @@ -612,6 +612,7 @@ func ValidateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi alert.TypeMatrix, alert.TypeMattermost, alert.TypeMessagebird, + alert.TypeN8N, alert.TypeNewRelic, alert.TypeNtfy, alert.TypeOpsgenie,