From ae750aa36748d7e868d1992fefb3255e0cb05057 Mon Sep 17 00:00:00 2001 From: michael-baraboo <37846855+michael-baraboo@users.noreply.github.com> Date: Thu, 28 Mar 2024 17:36:22 -0500 Subject: [PATCH] feat(alerting): add alerting support for jetbrains space (#713) * add alerting support for jetbrains space * readme fixes * add jetbrainsspace to provider interface compilation check * add jetbrainsspace to a couple more tests --- .github/assets/jetbrains-space-alerts.png | Bin 0 -> 31786 bytes README.md | 77 +++-- alerting/alert/type.go | 3 + alerting/config.go | 4 + alerting/provider/jetbrainsspace/space.go | 164 ++++++++++ .../provider/jetbrainsspace/space_test.go | 279 ++++++++++++++++++ alerting/provider/provider.go | 2 + config/config.go | 1 + config/config_test.go | 90 ++++-- watchdog/alerting_test.go | 12 + 10 files changed, 592 insertions(+), 40 deletions(-) create mode 100644 .github/assets/jetbrains-space-alerts.png create mode 100644 alerting/provider/jetbrainsspace/space.go create mode 100644 alerting/provider/jetbrainsspace/space_test.go diff --git a/.github/assets/jetbrains-space-alerts.png b/.github/assets/jetbrains-space-alerts.png new file mode 100644 index 0000000000000000000000000000000000000000..e339c2edb668a636eacc9cb97f0a9f3ef3ed907d GIT binary patch literal 31786 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 VF|5xTPWwh=BoxI9pBlXTe*izBm;nF) literal 0 HcmV?d00001 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,