diff --git a/README.md b/README.md index bc52cf04..11809690 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga - [Configuring Google Chat alerts](#configuring-google-chat-alerts) - [Configuring Gotify alerts](#configuring-gotify-alerts) - [Configuring HomeAssistant alerts](#configuring-homeassistant-alerts) + - [Configuring Ilert alerts](#configuring-ilert-alerts) - [Configuring Incident.io alerts](#configuring-incidentio-alerts) - [Configuring JetBrains Space alerts](#configuring-jetbrains-space-alerts) - [Configuring Matrix alerts](#configuring-matrix-alerts) @@ -590,6 +591,7 @@ endpoints: | `alerting.gitlab` | Configuration for alerts of type `gitlab`.
See [Configuring GitLab alerts](#configuring-gitlab-alerts). | `{}` | | `alerting.googlechat` | Configuration for alerts of type `googlechat`.
See [Configuring Google Chat alerts](#configuring-google-chat-alerts). | `{}` | | `alerting.gotify` | Configuration for alerts of type `gotify`.
See [Configuring Gotify alerts](#configuring-gotify-alerts). | `{}` | +| `alerting.ilert` | Configuration for alerts of type `ilert`.
See [Configuring ilert alerts](#configuring-ilert-alerts). | `{}` | | `alerting.incident-io` | Configuration for alerts of type `incident-io`.
See [Configuring Incident.io alerts](#configuring-incidentio-alerts). | `{}` | | `alerting.jetbrainsspace` | Configuration for alerts of type `jetbrainsspace`.
See [Configuring JetBrains Space alerts](#configuring-jetbrains-space-alerts). | `{}` | | `alerting.matrix` | Configuration for alerts of type `matrix`.
See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` | @@ -897,6 +899,51 @@ endpoints: | `alerting.gotify.title` | Title of the notification | `"Gatus: "` | | `alerting.gotify.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert). | N/A | +#### Configuring ilert alerts +| Parameter | Description | Default | +|:---------------------------------------|:-------------------------------------------------------------------------------------------|:--------| +| `alerting.ilert` | Configuration for alerts of type `ilert` | `{}` | +| `alerting.ilert.integration-key` | ilert Alert Source integration key | `""` | +| `alerting.ilert.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert) | N/A | +| `alerting.ilert.overrides` | List of overrides that may be prioritized over the default configuration | `[]` | +| `alerting.ilert.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` | +| `alerting.ilert.overrides[].*` | See `alerting.ilert.*` parameters | `{}` | + +It is highly recommended to set `endpoints[].alerts[].send-on-resolved` to `true` for alerts +of type `ilert`, because unlike other alerts, the operation resulting from setting said +parameter to `true` will not create another alert but mark the alert as resolved on +ilert instead. + +Behavior: +- By default, `alerting.ilert.integration-key` is used as the integration key +- If the endpoint being evaluated belongs to a group (`endpoints[].group`) matching the value of `alerting.ilert.overrides[].group`, the provider will use that override's integration key instead of `alerting.ilert.integration-key`'s + +```yaml +alerting: + ilert: + integration-key: "********************************" + # You can also add group-specific integration keys, which will + # override the integration key above for the specified groups + overrides: + - group: "core" + integration-key: "********************************" + +endpoints: + - name: website + url: "https://twin.sh/health" + interval: 30s + conditions: + - "[STATUS] == 200" + - "[BODY].status == UP" + - "[RESPONSE_TIME] < 300" + alerts: + - type: ilert + failure-threshold: 3 + success-threshold: 5 + send-on-resolved: true + description: "healthcheck failed" +``` + ```yaml alerting: gotify: diff --git a/alerting/alert/type.go b/alerting/alert/type.go index 1f36d6e1..1ec49c32 100644 --- a/alerting/alert/type.go +++ b/alerting/alert/type.go @@ -32,8 +32,11 @@ const ( // TypeGotify is the Type for the gotify alerting provider TypeGotify Type = "gotify" - // TypeHomeAssistant is the Type for the homeassistant alerting provider + // TypeHomeAssistant is the Type for the homeassistant alerting provider TypeHomeAssistant Type = "homeassistant" + + // TypeIlert is the Type for the ilert alerting provider + TypeIlert Type = "ilert" // TypeIncidentIO is the Type for the incident-io alerting provider TypeIncidentIO Type = "incident-io" diff --git a/alerting/config.go b/alerting/config.go index 58c1d647..30ca1e07 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -15,7 +15,8 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/gitlab" "github.com/TwiN/gatus/v5/alerting/provider/googlechat" "github.com/TwiN/gatus/v5/alerting/provider/gotify" - "github.com/TwiN/gatus/v5/alerting/provider/homeassistant" + "github.com/TwiN/gatus/v5/alerting/provider/homeassistant" + "github.com/TwiN/gatus/v5/alerting/provider/ilert" "github.com/TwiN/gatus/v5/alerting/provider/incidentio" "github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace" "github.com/TwiN/gatus/v5/alerting/provider/matrix" @@ -62,9 +63,12 @@ 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"` + + // Ilert is the configuration for the ilert alerting provider + Ilert *ilert.AlertProvider `yaml:"ilert,omitempty"` // IncidentIO is the configuration for the incident-io alerting provider IncidentIO *incidentio.AlertProvider `yaml:"incident-io,omitempty"` diff --git a/alerting/provider/ilert/ilert.go b/alerting/provider/ilert/ilert.go new file mode 100644 index 00000000..ba1822b5 --- /dev/null +++ b/alerting/provider/ilert/ilert.go @@ -0,0 +1,168 @@ +package ilert + +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" +) + +const ( + restAPIUrl = "https://api.ilert.com/api/v1/events/gatus/" +) + +var ( + ErrIntegrationKeyNotSet = errors.New("integration key is not set") + ErrDuplicateGroupOverride = errors.New("duplicate group override") +) + +type Config struct { + IntegrationKey string `yaml:"integration-key"` +} + +func (cfg *Config) Validate() error { + if len(cfg.IntegrationKey) == 0 { + return ErrIntegrationKeyNotSet + } + return nil +} + +func (cfg *Config) Merge(override *Config) { + if len(override.IntegrationKey) > 0 { + cfg.IntegrationKey = override.IntegrationKey + } +} + +// AlertProvider is the configuration necessary for sending an alert using ilert +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"` +} + +type Override struct { + Group string `yaml:"group"` + Config `yaml:",inline"` +} + +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() +} + +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)) + + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", restAPIUrl, cfg.IntegrationKey), buffer) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + response, err := client.GetHTTPClient(nil).Do(req) + 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 { + Alert alert.Alert `json:"alert"` + Name string `json:"name"` + Group string `json:"group"` + Status string `json:"status"` + Title string `json:"title"` + Details string `json:"details,omitempty"` + ConditionResults []*endpoint.ConditionResult `json:"condition_results"` + URL string `json:"url"` +} + +func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte { + var details, status string + if resolved { + status = "resolved" + } else { + status = "firing" + } + + if len(alert.GetDescription()) > 0 { + details = alert.GetDescription() + } else { + details = "No description" + } + + var body []byte + body, _ = json.Marshal(Body{ + Alert: *alert, + Name: ep.Name, + Group: ep.Group, + Title: ep.DisplayName(), + Status: status, + Details: details, + ConditionResults: result.ConditionResults, + URL: ep.URL, + }) + return body +} + +func (provider *AlertProvider) GetDefaultAlert() *alert.Alert { + return provider.DefaultAlert +} + +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/ilert/ilert_test.go b/alerting/provider/ilert/ilert_test.go new file mode 100644 index 00000000..3f18d60e --- /dev/null +++ b/alerting/provider/ilert/ilert_test.go @@ -0,0 +1,322 @@ +package ilert + +import ( + "bytes" + "encoding/json" + "io" + "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) { + scenarios := []struct { + name string + provider AlertProvider + expected bool + }{ + { + name: "valid", + provider: AlertProvider{ + DefaultConfig: Config{ + IntegrationKey: "some-random-key", + }, + }, + expected: true, + }, + { + name: "invalid-integration-key", + provider: AlertProvider{ + DefaultConfig: Config{ + IntegrationKey: "", + }, + }, + expected: false, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + err := scenario.provider.Validate() + if scenario.expected && err != nil { + t.Error("expected no error, got", err.Error()) + } + if !scenario.expected && err == nil { + t.Error("expected error, got none") + } + }) + } +} + +func TestAlertProvider_ValidateWithOverride(t *testing.T) { + providerWithInvalidOverrideGroup := AlertProvider{ + DefaultConfig: Config{IntegrationKey: "00000000000000000000000000000001"}, + Overrides: []Override{ + { + Config: Config{IntegrationKey: "00000000000000000000000000000002"}, + Group: "", + }, + }, + } + if err := providerWithInvalidOverrideGroup.Validate(); err == nil { + t.Error("provider Group shouldn't have been valid") + } + providerWithValidOverride := AlertProvider{ + DefaultConfig: Config{IntegrationKey: "00000000000000000000000000000001"}, + Overrides: []Override{ + { + Config: Config{IntegrationKey: "00000000000000000000000000000002"}, + Group: "group", + }, + }, + } + if err := providerWithValidOverride.Validate(); err != nil { + t.Error("provider should've been valid, got error:", err.Error()) + } +} + +func TestAlertProvider_Send(t *testing.T) { + defer client.InjectHTTPClient(nil) + firstDescription := "description-1" + secondDescription := "description-2" + sendOnResolved := true + scenarios := []struct { + Name string + Provider AlertProvider + Alert alert.Alert + Resolved bool + MockRoundTripper test.MockRoundTripper + ExpectedError bool + }{ + { + Name: "triggered", + Provider: AlertProvider{DefaultConfig: Config{ + IntegrationKey: "some-integration-key", + }}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 3, FailureThreshold: 3, ResolveKey: "123", Type: "ilert", SendOnResolved: &sendOnResolved}, + Resolved: false, + MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response { + var b bytes.Buffer + + reader := io.NopCloser(&b) + return &http.Response{StatusCode: http.StatusAccepted, Body: reader} + }), + ExpectedError: false, + }, + { + Name: "triggered-error", + Provider: AlertProvider{DefaultConfig: Config{ + IntegrationKey: "some-integration-key", + }}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 3, FailureThreshold: 3, ResolveKey: "123", Type: "ilert", SendOnResolved: &sendOnResolved}, + Resolved: false, + MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response { + return &http.Response{StatusCode: http.StatusBadRequest, Body: http.NoBody} + }), + ExpectedError: true, + }, + { + Name: "resolved", + Provider: AlertProvider{DefaultConfig: Config{ + IntegrationKey: "some-integration-key", + }}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 3, FailureThreshold: 3, ResolveKey: "123", Type: "ilert", SendOnResolved: &sendOnResolved}, + Resolved: true, + MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response { + var b bytes.Buffer + reader := io.NopCloser(&b) + return &http.Response{StatusCode: http.StatusAccepted, Body: reader} + }), + ExpectedError: false, + }, + } + 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" + sendOnResolved := true + + scenarios := []struct { + Name string + Provider AlertProvider + Alert alert.Alert + Resolved bool + ExpectedBody string + }{ + { + Name: "triggered", + Provider: AlertProvider{DefaultConfig: Config{IntegrationKey: "some-integration-key"}}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 3, FailureThreshold: 3, ResolveKey: "123", Type: "ilert", SendOnResolved: &sendOnResolved}, + Resolved: false, + ExpectedBody: `{"alert":{"Type":"ilert","Enabled":null,"FailureThreshold":3,"SuccessThreshold":3,"Description":"description-1","SendOnResolved":true,"ProviderOverride":null,"ResolveKey":"123","Triggered":false},"name":"endpoint-name","group":"","status":"firing","title":"endpoint-name","details":"description-1","condition_results":[{"condition":"[CONNECTED] == true","success":false},{"condition":"[STATUS] == 200","success":false}],"url":""}`, + }, + { + Name: "resolved", + Provider: AlertProvider{DefaultConfig: Config{IntegrationKey: "some-integration-key"}}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 4, FailureThreshold: 3, ResolveKey: "123", Type: "ilert", SendOnResolved: &sendOnResolved}, + Resolved: true, + ExpectedBody: `{"alert":{"Type":"ilert","Enabled":null,"FailureThreshold":3,"SuccessThreshold":4,"Description":"description-1","SendOnResolved":true,"ProviderOverride":null,"ResolveKey":"123","Triggered":false},"name":"endpoint-name","group":"","status":"resolved","title":"endpoint-name","details":"description-1","condition_results":[{"condition":"[CONNECTED] == true","success":true},{"condition":"[STATUS] == 200","success":true}],"url":""}`, + }, + { + Name: "group-override", + Provider: AlertProvider{DefaultConfig: Config{IntegrationKey: "some-integration-key"}, Overrides: []Override{{Group: "g", Config: Config{IntegrationKey: "different-integration-key"}}}}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3, ResolveKey: "123", Type: "ilert", SendOnResolved: &sendOnResolved}, + Resolved: false, + ExpectedBody: `{"alert":{"Type":"ilert","Enabled":null,"FailureThreshold":3,"SuccessThreshold":5,"Description":"description-2","SendOnResolved":true,"ProviderOverride":null,"ResolveKey":"123","Triggered":false},"name":"endpoint-name","group":"","status":"firing","title":"endpoint-name","details":"description-2","condition_results":[{"condition":"[CONNECTED] == true","success":false},{"condition":"[STATUS] == 200","success":false}],"url":""}`, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + cfg, err := scenario.Provider.GetConfig("g", &scenario.Alert) + if err != nil { + t.Error("expected no error, got", err.Error()) + } + body := scenario.Provider.buildRequestBody( + cfg, + &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 string(body) != scenario.ExpectedBody { + t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body) + } + out := make(map[string]interface{}) + if err := json.Unmarshal(body, &out); err != nil { + t.Error("expected body to be valid JSON, got error:", err.Error()) + } + }) + } +} + +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{IntegrationKey: "00000000000000000000000000000001"}, + Overrides: nil, + }, + InputGroup: "", + InputAlert: alert.Alert{}, + ExpectedOutput: Config{IntegrationKey: "00000000000000000000000000000001"}, + }, + { + Name: "provider-no-override-specify-group-should-default", + Provider: AlertProvider{ + DefaultConfig: Config{IntegrationKey: "00000000000000000000000000000001"}, + Overrides: nil, + }, + InputGroup: "group", + InputAlert: alert.Alert{}, + ExpectedOutput: Config{IntegrationKey: "00000000000000000000000000000001"}, + }, + { + Name: "provider-with-override-specify-no-group-should-default", + Provider: AlertProvider{ + DefaultConfig: Config{IntegrationKey: "00000000000000000000000000000001"}, + Overrides: []Override{ + { + Group: "group", + Config: Config{IntegrationKey: "00000000000000000000000000000002"}, + }, + }, + }, + InputGroup: "", + InputAlert: alert.Alert{}, + ExpectedOutput: Config{IntegrationKey: "00000000000000000000000000000001"}, + }, + { + Name: "provider-with-override-specify-group-should-override", + Provider: AlertProvider{ + DefaultConfig: Config{IntegrationKey: "00000000000000000000000000000001"}, + Overrides: []Override{ + { + Group: "group", + Config: Config{IntegrationKey: "00000000000000000000000000000002"}, + }, + }, + }, + InputGroup: "group", + InputAlert: alert.Alert{}, + ExpectedOutput: Config{IntegrationKey: "00000000000000000000000000000002"}, + }, + { + Name: "provider-with-group-override-and-alert-override--alert-override-should-take-precedence", + Provider: AlertProvider{ + DefaultConfig: Config{IntegrationKey: "00000000000000000000000000000001"}, + Overrides: []Override{ + { + Group: "group", + Config: Config{IntegrationKey: "00000000000000000000000000000002"}, + }, + }, + }, + InputGroup: "group", + InputAlert: alert.Alert{ProviderOverride: map[string]any{"integration-key": "00000000000000000000000000000003"}}, + ExpectedOutput: Config{IntegrationKey: "00000000000000000000000000000003"}, + }, + } + 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.IntegrationKey != scenario.ExpectedOutput.IntegrationKey { + t.Errorf("expected %s, got %s", scenario.ExpectedOutput.IntegrationKey, got.IntegrationKey) + } + // 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 227fc6df..1b76dfd5 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -10,8 +10,9 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/github" "github.com/TwiN/gatus/v5/alerting/provider/gitlab" "github.com/TwiN/gatus/v5/alerting/provider/googlechat" - "github.com/TwiN/gatus/v5/alerting/provider/gotify" + "github.com/TwiN/gatus/v5/alerting/provider/gotify" "github.com/TwiN/gatus/v5/alerting/provider/homeassistant" + "github.com/TwiN/gatus/v5/alerting/provider/ilert" "github.com/TwiN/gatus/v5/alerting/provider/incidentio" "github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace" "github.com/TwiN/gatus/v5/alerting/provider/matrix" @@ -82,8 +83,9 @@ var ( _ AlertProvider = (*github.AlertProvider)(nil) _ AlertProvider = (*gitlab.AlertProvider)(nil) _ AlertProvider = (*googlechat.AlertProvider)(nil) - _ AlertProvider = (*gotify.AlertProvider)(nil) + _ AlertProvider = (*gotify.AlertProvider)(nil) _ AlertProvider = (*homeassistant.AlertProvider)(nil) + _ AlertProvider = (*ilert.AlertProvider)(nil) _ AlertProvider = (*incidentio.AlertProvider)(nil) _ AlertProvider = (*jetbrainsspace.AlertProvider)(nil) _ AlertProvider = (*matrix.AlertProvider)(nil) @@ -109,8 +111,9 @@ var ( _ Config[github.Config] = (*github.Config)(nil) _ Config[gitlab.Config] = (*gitlab.Config)(nil) _ Config[googlechat.Config] = (*googlechat.Config)(nil) - _ Config[gotify.Config] = (*gotify.Config)(nil) + _ Config[gotify.Config] = (*gotify.Config)(nil) _ Config[homeassistant.Config] = (*homeassistant.Config)(nil) + _ Config[ilert.Config] = (*ilert.Config)(nil) _ Config[incidentio.Config] = (*incidentio.Config)(nil) _ Config[jetbrainsspace.Config] = (*jetbrainsspace.Config)(nil) _ Config[matrix.Config] = (*matrix.Config)(nil) diff --git a/config/config.go b/config/config.go index 40b50d72..608a1384 100644 --- a/config/config.go +++ b/config/config.go @@ -411,7 +411,8 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi alert.TypeGitea, alert.TypeGoogleChat, alert.TypeGotify, - alert.TypeHomeAssistant, + alert.TypeHomeAssistant, + alert.TypeIlert, alert.TypeIncidentIO, alert.TypeJetBrainsSpace, alert.TypeMatrix,