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,