diff --git a/.github/assets/jetbrains-space-alerts.png b/.github/assets/jetbrains-space-alerts.png new file mode 100644 index 00000000..e339c2ed Binary files /dev/null and b/.github/assets/jetbrains-space-alerts.png differ diff --git a/README.md b/README.md index 74c9d3cf..e29f4748 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga - [Configuring GitLab alerts](#configuring-gitlab-alerts) - [Configuring Google Chat alerts](#configuring-google-chat-alerts) - [Configuring Gotify alerts](#configuring-gotify-alerts) + - [Configuring JetBrains Space alerts](#configuring-jetbrains-space-alerts) - [Configuring Matrix alerts](#configuring-matrix-alerts) - [Configuring Mattermost alerts](#configuring-mattermost-alerts) - [Configuring Messagebird alerts](#configuring-messagebird-alerts) @@ -444,26 +445,27 @@ individual endpoints with configurable descriptions and thresholds. > 📝 If an alerting provider is not properly configured, all alerts configured with the provider's type will be ignored. -| Parameter | Description | Default | -|:-----------------------|:-----------------------------------------------------------------------------------------------------------------------------|:--------| -| `alerting.custom` | Configuration for custom actions on failure or alerts.
See [Configuring Custom alerts](#configuring-custom-alerts). | `{}` | -| `alerting.discord` | Configuration for alerts of type `discord`.
See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` | -| `alerting.email` | Configuration for alerts of type `email`.
See [Configuring Email alerts](#configuring-email-alerts). | `{}` | -| `alerting.github` | Configuration for alerts of type `github`.
See [Configuring GitHub alerts](#configuring-github-alerts). | `{}` | -| `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.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.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). | `{}` | -| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`.
See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` | -| `alerting.pushover` | Configuration for alerts of type `pushover`.
See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` | -| `alerting.slack` | Configuration for alerts of type `slack`.
See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` | -| `alerting.teams` | Configuration for alerts of type `teams`.
See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` | -| `alerting.telegram` | Configuration for alerts of type `telegram`.
See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` | -| `alerting.twilio` | Settings for alerts of type `twilio`.
See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` | +| Parameter | Description | Default | +|:--------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------|:--------| +| `alerting.custom` | Configuration for custom actions on failure or alerts.
See [Configuring Custom alerts](#configuring-custom-alerts). | `{}` | +| `alerting.discord` | Configuration for alerts of type `discord`.
See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` | +| `alerting.email` | Configuration for alerts of type `email`.
See [Configuring Email alerts](#configuring-email-alerts). | `{}` | +| `alerting.github` | Configuration for alerts of type `github`.
See [Configuring GitHub alerts](#configuring-github-alerts). | `{}` | +| `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.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). | `{}` | +| `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.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). | `{}` | +| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`.
See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` | +| `alerting.pushover` | Configuration for alerts of type `pushover`.
See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` | +| `alerting.slack` | Configuration for alerts of type `slack`.
See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` | +| `alerting.teams` | Configuration for alerts of type `teams`.
See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` | +| `alerting.telegram` | Configuration for alerts of type `telegram`.
See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` | +| `alerting.twilio` | Settings for alerts of type `twilio`.
See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` | #### Configuring Discord alerts @@ -703,6 +705,41 @@ Here's an example of what the notifications look like: ![Gotify notifications](.github/assets/gotify-alerts.png) +#### Configuring JetBrains Space alerts +| Parameter | Description | Default | +|:---------------------------------------------------|:--------------------------------------------------------------------------------------------|:-----------------------| +| `alerting.jetbrainsspace` | Configuration for alerts of type `jetbrainsspace` | `{}` | +| `alerting.jetbrainsspace.project` | JetBrains Space project name | Required `""` | +| `alerting.jetbrainsspace.channel-id` | JetBrains Space Chat Channel ID | Required `""` | +| `alerting.jetbrainsspace.token` | Token that is used for authentication. | Required `""` | +| `alerting.jetbrainsspace.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert) | N/A | +| `alerting.jetbrainsspace.overrides` | List of overrides that may be prioritized over the default configuration | `[]` | +| `alerting.jetbrainsspace.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` | + +```yaml +alerting: + jetbrainsspace: + project: myproject + channel-id: ABCDE12345 + token: "**************" + +endpoints: + - name: website + url: "https://twin.sh/health" + interval: 5m + conditions: + - "[STATUS] == 200" + alerts: + - type: jetbrainsspace + description: "healthcheck failed" + send-on-resolved: true +``` + +Here's an example of what the notifications look like: + +![JetBrains Space notifications](.github/assets/jetbrains-space-alerts.png) + + #### Configuring Matrix alerts | Parameter | Description | Default | |:-----------------------------------------|:-------------------------------------------------------------------------------------------|:-----------------------------------| diff --git a/alerting/alert/type.go b/alerting/alert/type.go index 22f7b8d6..51febc00 100644 --- a/alerting/alert/type.go +++ b/alerting/alert/type.go @@ -29,6 +29,9 @@ const ( // TypeGotify is the Type for the gotify alerting provider TypeGotify Type = "gotify" + // TypeJetBrainsSpace is the Type for the jetbrains alerting provider + TypeJetBrainsSpace Type = "jetbrainsspace" + // TypeMatrix is the Type for the matrix alerting provider TypeMatrix Type = "matrix" diff --git a/alerting/config.go b/alerting/config.go index af52115d..453183d7 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -15,6 +15,7 @@ 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/jetbrainsspace" "github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/mattermost" "github.com/TwiN/gatus/v5/alerting/provider/messagebird" @@ -54,6 +55,9 @@ type Config struct { // Gotify is the configuration for the gotify alerting provider Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"` + // JetBrainsSpace is the configuration for the jetbrains space alerting provider + JetBrainsSpace *jetbrainsspace.AlertProvider `yaml:"jetbrainsspace,omitempty"` + // Matrix is the configuration for the matrix alerting provider Matrix *matrix.AlertProvider `yaml:"matrix,omitempty"` diff --git a/alerting/provider/jetbrainsspace/space.go b/alerting/provider/jetbrainsspace/space.go new file mode 100644 index 00000000..55046450 --- /dev/null +++ b/alerting/provider/jetbrainsspace/space.go @@ -0,0 +1,164 @@ +package jetbrainsspace + +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" +) + +// AlertProvider is the configuration necessary for sending an alert using JetBrains Space +type AlertProvider struct { + Project string `yaml:"project"` // JetBrains Space Project name + ChannelID string `yaml:"channel-id"` // JetBrains Space Chat Channel ID + Token string `yaml:"token"` // JetBrains Space Bearer Token + // DefaultAlert is the defarlt 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"` + ChannelID string `yaml:"channel-id"` +} + +// IsValid returns whether the provider's configuration is valid +func (provider *AlertProvider) IsValid() bool { + registeredGroups := make(map[string]bool) + if provider.Overrides != nil { + for _, override := range provider.Overrides { + if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.ChannelID) == 0 { + return false + } + registeredGroups[override.Group] = true + } + } + return len(provider.Project) > 0 && len(provider.ChannelID) > 0 && len(provider.Token) > 0 +} + +// Send an alert using the provider +func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { + buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) + url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project) + request, err := http.NewRequest(http.MethodPost, url, buffer) + if err != nil { + return err + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", "Bearer "+provider.Token) + 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 { + Channel string `json:"channel"` + Content Content `json:"content"` +} + +type Content struct { + ClassName string `json:"className"` + Style string `json:"style"` + Sections []Section `json:"sections"` +} + +type Section struct { + ClassName string `json:"className"` + Elements []Element `json:"elements"` + Header string `json:"header"` +} + +type Element struct { + ClassName string `json:"className"` + Accessory Accessory `json:"accessory"` + Style string `json:"style"` + Size string `json:"size"` + Content string `json:"content"` +} + +type Accessory struct { + ClassName string `json:"className"` + Icon Icon `json:"icon"` + Style string `json:"style"` +} + +type Icon struct { + Icon string `json:"icon"` +} + +// buildRequestBody builds the request body for the provider +func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { + body := Body{ + Channel: "id:" + provider.getChannelIDForGroup(endpoint.Group), + Content: Content{ + ClassName: "ChatMessage.Block", + Sections: []Section{{ + ClassName: "MessageSection", + Elements: []Element{}, + }}, + }, + } + + if resolved { + body.Content.Style = "SUCCESS" + body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) + } else { + body.Content.Style = "WARNING" + body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) + } + + for _, conditionResult := range result.ConditionResults { + icon := "warning" + style := "WARNING" + if conditionResult.Success { + icon = "success" + style = "SUCCESS" + } + + body.Content.Sections[0].Elements = append(body.Content.Sections[0].Elements, Element{ + ClassName: "MessageText", + Accessory: Accessory{ + ClassName: "MessageIcon", + Icon: Icon{Icon: icon}, + Style: style, + }, + Style: style, + Size: "REGULAR", + Content: conditionResult.Condition, + }) + } + + jsonBody, _ := json.Marshal(body) + return jsonBody +} + +// getChannelIDForGroup returns the appropriate channel ID to for a given group override +func (provider *AlertProvider) getChannelIDForGroup(group string) string { + if provider.Overrides != nil { + for _, override := range provider.Overrides { + if group == override.Group { + return override.ChannelID + } + } + } + return provider.ChannelID +} + +// GetDefaultAlert returns the provider's default alert configuration +func (provider *AlertProvider) GetDefaultAlert() *alert.Alert { + return provider.DefaultAlert +} diff --git a/alerting/provider/jetbrainsspace/space_test.go b/alerting/provider/jetbrainsspace/space_test.go new file mode 100644 index 00000000..8eae2590 --- /dev/null +++ b/alerting/provider/jetbrainsspace/space_test.go @@ -0,0 +1,279 @@ +package jetbrainsspace + +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 TestAlertDefaultProvider_IsValid(t *testing.T) { + invalidProvider := AlertProvider{Project: ""} + if invalidProvider.IsValid() { + t.Error("provider shouldn't have been valid") + } + validProvider := AlertProvider{Project: "foo", ChannelID: "bar", Token: "baz"} + if !validProvider.IsValid() { + t.Error("provider should've been valid") + } +} + +func TestAlertProvider_IsValidWithOverride(t *testing.T) { + providerWithInvalidOverrideGroup := AlertProvider{ + Project: "foobar", + Overrides: []Override{ + { + ChannelID: "http://example.com", + Group: "", + }, + }, + } + if providerWithInvalidOverrideGroup.IsValid() { + t.Error("provider Group shouldn't have been valid") + } + providerWithInvalidOverrideTo := AlertProvider{ + Project: "foobar", + Overrides: []Override{ + { + ChannelID: "", + Group: "group", + }, + }, + } + if providerWithInvalidOverrideTo.IsValid() { + t.Error("provider integration key shouldn't have been valid") + } + providerWithValidOverride := AlertProvider{ + Project: "foo", + ChannelID: "bar", + Token: "baz", + Overrides: []Override{ + { + ChannelID: "foobar", + Group: "group", + }, + }, + } + if !providerWithValidOverride.IsValid() { + 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{}, + 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 + Endpoint core.Endpoint + Alert alert.Alert + Resolved bool + ExpectedBody string + }{ + { + Name: "triggered", + Provider: AlertProvider{}, + Endpoint: core.Endpoint{Name: "name"}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been triggered due to having failed 3 time(s) in a row"}]}}`, + }, + { + Name: "triggered-with-group", + Provider: AlertProvider{}, + Endpoint: core.Endpoint{Name: "name", Group: "group"}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row"}]}}`, + }, + { + Name: "resolved", + Provider: AlertProvider{}, + Endpoint: core.Endpoint{Name: "name"}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: true, + ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been resolved after passing successfully 5 time(s) in a row"}]}}`, + }, + { + Name: "resolved-with-group", + Provider: AlertProvider{}, + Endpoint: core.Endpoint{Name: "name", Group: "group"}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: true, + ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row"}]}}`, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + body := scenario.Provider.buildRequestBody( + &scenario.Endpoint, + &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(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_getChannelIDForGroup(t *testing.T) { + tests := []struct { + Name string + Provider AlertProvider + InputGroup string + ExpectedOutput string + }{ + { + Name: "provider-no-override-specify-no-group-should-default", + Provider: AlertProvider{ + ChannelID: "bar", + }, + InputGroup: "", + ExpectedOutput: "bar", + }, + { + Name: "provider-no-override-specify-group-should-default", + Provider: AlertProvider{ + ChannelID: "bar", + }, + InputGroup: "group", + ExpectedOutput: "bar", + }, + { + Name: "provider-with-override-specify-no-group-should-default", + Provider: AlertProvider{ + ChannelID: "bar", + Overrides: []Override{ + { + Group: "group", + ChannelID: "foobar", + }, + }, + }, + InputGroup: "", + ExpectedOutput: "bar", + }, + { + Name: "provider-with-override-specify-group-should-override", + Provider: AlertProvider{ + ChannelID: "bar", + Overrides: []Override{ + { + Group: "group", + ChannelID: "foobar", + }, + }, + }, + InputGroup: "group", + ExpectedOutput: "foobar", + }, + } + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + if got := tt.Provider.getChannelIDForGroup(tt.InputGroup); got != tt.ExpectedOutput { + t.Errorf("AlertProvider.getChannelIDForGroup() = %v, want %v", got, tt.ExpectedOutput) + } + }) + } +} diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go index ff82e501..5059e0f0 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -9,6 +9,7 @@ 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/jetbrainsspace" "github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/mattermost" "github.com/TwiN/gatus/v5/alerting/provider/messagebird" @@ -66,6 +67,7 @@ var ( _ AlertProvider = (*github.AlertProvider)(nil) _ AlertProvider = (*gitlab.AlertProvider)(nil) _ AlertProvider = (*googlechat.AlertProvider)(nil) + _ AlertProvider = (*jetbrainsspace.AlertProvider)(nil) _ AlertProvider = (*matrix.AlertProvider)(nil) _ AlertProvider = (*mattermost.AlertProvider)(nil) _ AlertProvider = (*messagebird.AlertProvider)(nil) diff --git a/config/config.go b/config/config.go index 70f62f2c..9462c62d 100644 --- a/config/config.go +++ b/config/config.go @@ -368,6 +368,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E alert.TypeGitLab, alert.TypeGoogleChat, alert.TypeGotify, + alert.TypeJetBrainsSpace, alert.TypeEmail, alert.TypeMatrix, alert.TypeMattermost, diff --git a/config/config_test.go b/config/config_test.go index ef1068a7..e56fb8d4 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -16,6 +16,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/email" "github.com/TwiN/gatus/v5/alerting/provider/github" "github.com/TwiN/gatus/v5/alerting/provider/googlechat" + "github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace" "github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/mattermost" "github.com/TwiN/gatus/v5/alerting/provider/messagebird" @@ -706,6 +707,10 @@ alerting: to: "+1-234-567-8901" teams: webhook-url: "http://example.com" + jetbrainsspace: + project: "foo" + channel-id: "bar" + token: "baz" endpoints: - name: website @@ -728,6 +733,7 @@ endpoints: success-threshold: 15 - type: teams - type: pushover + - type: jetbrainsspace conditions: - "[STATUS] == 200" `)) @@ -754,8 +760,8 @@ endpoints: if config.Endpoints[0].Interval != 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) != 9 { - t.Fatal("There should've been 9 alerts configured") + if len(config.Endpoints[0].Alerts) != 10 { + t.Fatal("There should've been 10 alerts configured") } if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack { @@ -862,6 +868,12 @@ endpoints: if !config.Endpoints[0].Alerts[8].IsEnabled() { t.Error("The alert should've been enabled") } + if config.Endpoints[0].Alerts[9].Type != alert.TypeJetBrainsSpace { + t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeJetBrainsSpace, config.Endpoints[0].Alerts[9].Type) + } + if !config.Endpoints[0].Alerts[9].IsEnabled() { + t.Error("The alert should've been enabled") + } } func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) { @@ -923,6 +935,14 @@ alerting: webhook-url: "http://example.com" default-alert: enabled: true + jetbrainsspace: + project: "foo" + channel-id: "bar" + token: "baz" + default-alert: + enabled: true + failure-threshold: 5 + success-threshold: 3 endpoints: - name: website @@ -938,6 +958,7 @@ endpoints: - type: twilio - type: teams - type: pushover + - type: jetbrainsspace conditions: - "[STATUS] == 200" `)) @@ -1049,6 +1070,21 @@ endpoints: if config.Alerting.Teams.GetDefaultAlert() == nil { t.Fatal("Teams.GetDefaultAlert() shouldn't have returned nil") } + if config.Alerting.JetBrainsSpace == nil || !config.Alerting.JetBrainsSpace.IsValid() { + t.Fatal("JetBrainsSpace alerting config should've been valid") + } + if config.Alerting.JetBrainsSpace.GetDefaultAlert() == nil { + t.Fatal("JetBrainsSpace.GetDefaultAlert() shouldn't have returned nil") + } + if config.Alerting.JetBrainsSpace.Project != "foo" { + t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "foo", config.Alerting.JetBrainsSpace.Project) + } + if config.Alerting.JetBrainsSpace.ChannelID != "bar" { + t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "bar", config.Alerting.JetBrainsSpace.ChannelID) + } + if config.Alerting.JetBrainsSpace.Token != "baz" { + t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "baz", config.Alerting.JetBrainsSpace.Token) + } // Endpoints if len(config.Endpoints) != 1 { @@ -1060,8 +1096,8 @@ endpoints: if config.Endpoints[0].Interval != 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) != 9 { - t.Fatal("There should've been 9 alerts configured") + if len(config.Endpoints[0].Alerts) != 10 { + t.Fatal("There should've been 10 alerts configured") } if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack { @@ -1178,6 +1214,18 @@ endpoints: t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[8].SuccessThreshold) } + if config.Endpoints[0].Alerts[9].Type != alert.TypeJetBrainsSpace { + t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeJetBrainsSpace, config.Endpoints[0].Alerts[9].Type) + } + if !config.Endpoints[0].Alerts[9].IsEnabled() { + t.Error("The alert should've been enabled") + } + if config.Endpoints[0].Alerts[9].FailureThreshold != 5 { + t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[9].FailureThreshold) + } + if config.Endpoints[0].Alerts[9].SuccessThreshold != 3 { + t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[9].SuccessThreshold) + } } func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) { @@ -1570,22 +1618,23 @@ func TestParseAndValidateConfigBytesWithNoEndpoints(t *testing.T) { func TestGetAlertingProviderByAlertType(t *testing.T) { alertingConfig := &alerting.Config{ - Custom: &custom.AlertProvider{}, - Discord: &discord.AlertProvider{}, - Email: &email.AlertProvider{}, - GitHub: &github.AlertProvider{}, - GoogleChat: &googlechat.AlertProvider{}, - Matrix: &matrix.AlertProvider{}, - Mattermost: &mattermost.AlertProvider{}, - Messagebird: &messagebird.AlertProvider{}, - Ntfy: &ntfy.AlertProvider{}, - Opsgenie: &opsgenie.AlertProvider{}, - PagerDuty: &pagerduty.AlertProvider{}, - Pushover: &pushover.AlertProvider{}, - Slack: &slack.AlertProvider{}, - Telegram: &telegram.AlertProvider{}, - Twilio: &twilio.AlertProvider{}, - Teams: &teams.AlertProvider{}, + Custom: &custom.AlertProvider{}, + Discord: &discord.AlertProvider{}, + Email: &email.AlertProvider{}, + GitHub: &github.AlertProvider{}, + GoogleChat: &googlechat.AlertProvider{}, + JetBrainsSpace: &jetbrainsspace.AlertProvider{}, + Matrix: &matrix.AlertProvider{}, + Mattermost: &mattermost.AlertProvider{}, + Messagebird: &messagebird.AlertProvider{}, + Ntfy: &ntfy.AlertProvider{}, + Opsgenie: &opsgenie.AlertProvider{}, + PagerDuty: &pagerduty.AlertProvider{}, + Pushover: &pushover.AlertProvider{}, + Slack: &slack.AlertProvider{}, + Telegram: &telegram.AlertProvider{}, + Twilio: &twilio.AlertProvider{}, + Teams: &teams.AlertProvider{}, } scenarios := []struct { alertType alert.Type @@ -1596,6 +1645,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) { {alertType: alert.TypeEmail, expected: alertingConfig.Email}, {alertType: alert.TypeGitHub, expected: alertingConfig.GitHub}, {alertType: alert.TypeGoogleChat, expected: alertingConfig.GoogleChat}, + {alertType: alert.TypeJetBrainsSpace, expected: alertingConfig.JetBrainsSpace}, {alertType: alert.TypeMatrix, expected: alertingConfig.Matrix}, {alertType: alert.TypeMattermost, expected: alertingConfig.Mattermost}, {alertType: alert.TypeMessagebird, expected: alertingConfig.Messagebird}, diff --git a/watchdog/alerting_test.go b/watchdog/alerting_test.go index 88dd10a1..eb334601 100644 --- a/watchdog/alerting_test.go +++ b/watchdog/alerting_test.go @@ -9,6 +9,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/custom" "github.com/TwiN/gatus/v5/alerting/provider/discord" "github.com/TwiN/gatus/v5/alerting/provider/email" + "github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace" "github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/mattermost" "github.com/TwiN/gatus/v5/alerting/provider/messagebird" @@ -281,6 +282,17 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) { }, }, }, + { + Name: "jetbrainsspace", + AlertType: alert.TypeJetBrainsSpace, + AlertingConfig: &alerting.Config{ + JetBrainsSpace: &jetbrainsspace.AlertProvider{ + Project: "foo", + ChannelID: "bar", + Token: "baz", + }, + }, + }, { Name: "mattermost", AlertType: alert.TypeMattermost,