diff --git a/.github/assets/mattermost-alerts.png b/.github/assets/mattermost-alerts.png new file mode 100644 index 00000000..907b0f81 Binary files /dev/null and b/.github/assets/mattermost-alerts.png differ diff --git a/Dockerfile b/Dockerfile index 9fede2d6..d926f7c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,10 @@ ADD . ./ RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus . RUN apk --update add ca-certificates +# Run Tests inside docker image if you don't have a configured go environment +#RUN apk update && apk add --virtual build-dependencies build-base gcc +#RUN go test ./... -mod vendor + # Run the binary on an empty container FROM scratch COPY --from=builder /app/gatus . diff --git a/README.md b/README.md index 892364f5..e8889a59 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ core applications: https://status.twinnation.org/ - [Functions](#functions) - [Alerting](#alerting) - [Configuring Slack alerts](#configuring-slack-alerts) + - [Configuring Mattermost alerts](#configuring-mattermost-alerts) - [Configuring PagerDuty alerts](#configuring-pagerduty-alerts) - [Configuring Twilio alerts](#configuring-twilio-alerts) - [Configuring custom alerts](#configuring-custom-alerts) @@ -48,7 +49,7 @@ The main features of Gatus are: - **Highly flexible health check conditions**: While checking the response status may be enough for some use cases, Gatus goes much further and allows you to add conditions on the response time, the response body and even the IP address. - **Ability to use Gatus for user acceptance tests**: Thanks to the point above, you can leverage this application to create automated user acceptance tests. - **Very easy to configure**: Not only is the configuration designed to be as readable as possible, it's also extremely easy to add a new service or a new endpoint to monitor. -- **Alerting**: While having a pretty visual dashboard is useful to keep track of the state of your application(s), you probably don't want to stare at it all day. Thus, notifications via Slack, PagerDuty and Twilio are supported out of the box with the ability to configure a custom alerting provider for any needs you might have, whether it be a different provider or a custom application that manages automated rollbacks. +- **Alerting**: While having a pretty visual dashboard is useful to keep track of the state of your application(s), you probably don't want to stare at it all day. Thus, notifications via Slack, Mattermost, PagerDuty and Twilio are supported out of the box with the ability to configure a custom alerting provider for any needs you might have, whether it be a different provider or a custom application that manages automated rollbacks. - **Metrics** - **Low resource consumption**: As with most Go applications, the resource footprint that this application requires is negligibly small. - **Service auto discovery in Kubernetes** (ALPHA) @@ -102,7 +103,7 @@ Note that you can also add environment variables in the configuration file (i.e. | `services[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`) | `false` | | `services[].body` | Request body | `""` | | `services[].headers` | Request headers | `{}` | -| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `pagerduty`, `twilio`, `custom` | Required `""` | +| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `pagerduty`, `twilio`, `custom`, `mattermost` | Required `""` | | `services[].alerts[].enabled` | Whether to enable the alert | `false` | | `services[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert | `3` | | `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | `2` | @@ -111,6 +112,9 @@ Note that you can also add environment variables in the configuration file (i.e. | `alerting` | Configuration for alerting | `{}` | | `alerting.slack` | Configuration for alerts of type `slack` | `{}` | | `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` | +| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` | +| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` | +| `alerting.mattermost.insecure` | Whether to skip verifying the server's certificate chain and host name | `false` | | `alerting.pagerduty` | Configuration for alerts of type `pagerduty` | `{}` | | `alerting.pagerduty.integration-key` | PagerDuty Events API v2 integration key. | Required `""` | | `alerting.twilio` | Settings for alerts of type `twilio` | `{}` | @@ -210,6 +214,37 @@ Here's an example of what the notifications look like: ![Slack notifications](.github/assets/slack-alerts.png) +#### Configuring Mattermost alerts + +```yaml +alerting: + mattermost: + webhook-url: "http://**********/hooks/**********" + insecure: true +services: + - name: twinnation + url: "https://twinnation.org/health" + interval: 30s + alerts: + - type: mattermost + enabled: true + description: "healthcheck failed 3 times in a row" + send-on-resolved: true + - type: mattermost + enabled: true + failure-threshold: 5 + description: "healthcheck failed 5 times in a row" + send-on-resolved: true + conditions: + - "[STATUS] == 200" + - "[BODY].status == UP" + - "[RESPONSE_TIME] < 300" +``` + +Here's an example of what the notifications look like: + +![Mattermost notifications](.github/assets/mattermost-alerts.png) + #### Configuring PagerDuty alerts It is highly recommended to set `services[].alerts[].send-on-resolved` to `true` for alerts @@ -287,6 +322,7 @@ alerting: custom: url: "https://hooks.slack.com/services/**********/**********/**********" method: "POST" + insecure: true body: | { "text": "[ALERT_TRIGGERED_OR_RESOLVED]: [SERVICE_NAME] - [ALERT_DESCRIPTION]" diff --git a/alerting/config.go b/alerting/config.go index d008fa6a..8ebda379 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -4,6 +4,7 @@ import ( "github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/alerting/provider/pagerduty" "github.com/TwinProduction/gatus/alerting/provider/slack" + "github.com/TwinProduction/gatus/alerting/provider/mattermost" "github.com/TwinProduction/gatus/alerting/provider/twilio" ) @@ -12,6 +13,9 @@ type Config struct { // Slack is the configuration for the slack alerting provider Slack *slack.AlertProvider `yaml:"slack"` + // Mattermost is the configuration for the mattermost alerting provider + Mattermost *mattermost.AlertProvider `yaml:"mattermost"` + // Pagerduty is the configuration for the pagerduty alerting provider PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty"` diff --git a/alerting/provider/mattermost/mattermost.go b/alerting/provider/mattermost/mattermost.go new file mode 100644 index 00000000..99ef2375 --- /dev/null +++ b/alerting/provider/mattermost/mattermost.go @@ -0,0 +1,73 @@ +package mattermost + +import ( + "fmt" + "github.com/TwinProduction/gatus/alerting/provider/custom" + "github.com/TwinProduction/gatus/core" +) + +// AlertProvider is the configuration necessary for sending an alert using Mattermost +type AlertProvider struct { + WebhookURL string `yaml:"webhook-url"` + Insecure bool `yaml:"insecure,omitempty"` +} + +// IsValid returns whether the provider's configuration is valid +func (provider *AlertProvider) IsValid() bool { + return len(provider.WebhookURL) > 0 +} + +// ToCustomAlertProvider converts the provider into a custom.AlertProvider +func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, result *core.Result, resolved bool) *custom.AlertProvider { + var message string + var color string + if resolved { + message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", service.Name, alert.SuccessThreshold) + color = "#36A64F" + } else { + message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", service.Name, alert.FailureThreshold) + color = "#DD0000" + } + var results string + for _, conditionResult := range result.ConditionResults { + var prefix string + if conditionResult.Success { + prefix = ":white_check_mark: " + } else { + prefix = ":x:" + } + results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) + } + return &custom.AlertProvider{ + URL: provider.WebhookURL, + Method: "POST", + Insecure: provider.Insecure, + Body: fmt.Sprintf(`{ + "text": "", + "username": "gatus", + "icon_url": "https://raw.githubusercontent.com/TwinProduction/gatus/master/static/logo.png", + "attachments": [ + { + "title": ":rescue_worker_helmet: Gatus", + "fallback": "Gatus - %s", + "text": "%s:\n> %s", + "short": false, + "color": "%s", + "fields": [ + { + "title": "URL", + "value": "%s", + "short": false + }, + { + "title": "Condition results", + "value": "%s", + "short": false + } + ] + } + ] + }`, message, message, alert.Description, color, service.URL, results), + Headers: map[string]string{"Content-Type": "application/json"}, + } +} diff --git a/alerting/provider/mattermost/mattermost_test.go b/alerting/provider/mattermost/mattermost_test.go new file mode 100644 index 00000000..09985eb8 --- /dev/null +++ b/alerting/provider/mattermost/mattermost_test.go @@ -0,0 +1,40 @@ +package mattermost + +import ( + "github.com/TwinProduction/gatus/core" + "strings" + "testing" +) + +func TestAlertProvider_IsValid(t *testing.T) { + invalidProvider := AlertProvider{WebhookURL: ""} + if invalidProvider.IsValid() { + t.Error("provider shouldn't have been valid") + } + validProvider := AlertProvider{WebhookURL: "http://example.com"} + if !validProvider.IsValid() { + t.Error("provider should've been valid") + } +} + +func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { + provider := AlertProvider{WebhookURL: "http://example.com"} + customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true) + if customAlertProvider == nil { + t.Error("customAlertProvider shouldn't have been nil") + } + if !strings.Contains(customAlertProvider.Body, "resolved") { + t.Error("customAlertProvider.Body should've contained the substring resolved") + } +} + +func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) { + provider := AlertProvider{WebhookURL: "http://example.com"} + customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false) + if customAlertProvider == nil { + t.Error("customAlertProvider shouldn't have been nil") + } + if !strings.Contains(customAlertProvider.Body, "triggered") { + t.Error("customAlertProvider.Body should've contained the substring triggered") + } +} diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go index 06be60c6..68d4596d 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -4,6 +4,7 @@ import ( "github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/alerting/provider/pagerduty" "github.com/TwinProduction/gatus/alerting/provider/slack" + "github.com/TwinProduction/gatus/alerting/provider/mattermost" "github.com/TwinProduction/gatus/alerting/provider/twilio" "github.com/TwinProduction/gatus/core" ) @@ -22,5 +23,6 @@ var ( _ AlertProvider = (*custom.AlertProvider)(nil) _ AlertProvider = (*twilio.AlertProvider)(nil) _ AlertProvider = (*slack.AlertProvider)(nil) + _ AlertProvider = (*mattermost.AlertProvider)(nil) _ AlertProvider = (*pagerduty.AlertProvider)(nil) ) diff --git a/config/config.go b/config/config.go index 4cc28350..cf466c87 100644 --- a/config/config.go +++ b/config/config.go @@ -179,6 +179,7 @@ func validateAlertingConfig(config *Config) { } alertTypes := []core.AlertType{ core.SlackAlert, + core.MattermostAlert, core.TwilioAlert, core.PagerDutyAlert, core.CustomAlert, @@ -209,6 +210,12 @@ func GetAlertingProviderByAlertType(config *Config, alertType core.AlertType) pr return nil } return config.Alerting.Slack + case core.MattermostAlert: + if config.Alerting.Mattermost == nil { + // Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil + return nil + } + return config.Alerting.Mattermost case core.TwilioAlert: if config.Alerting.Twilio == nil { // Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil diff --git a/core/alert.go b/core/alert.go index 99b12583..2ba882fa 100644 --- a/core/alert.go +++ b/core/alert.go @@ -37,6 +37,9 @@ const ( // SlackAlert is the AlertType for the slack alerting provider SlackAlert AlertType = "slack" + // MattermostAlert is the AlertType for the mattermost alerting provider + MattermostAlert AlertType = "mattermost" + // PagerDutyAlert is the AlertType for the pagerduty alerting provider PagerDutyAlert AlertType = "pagerduty" diff --git a/example/docker-compose-mattermost/config.yaml b/example/docker-compose-mattermost/config.yaml new file mode 100644 index 00000000..431be6aa --- /dev/null +++ b/example/docker-compose-mattermost/config.yaml @@ -0,0 +1,6 @@ +services: + - name: example + url: http://example.org + interval: 30s + conditions: + - "[STATUS] == 200" diff --git a/example/docker-compose-mattermost/docker-compose.yml b/example/docker-compose-mattermost/docker-compose.yml new file mode 100644 index 00000000..eea33871 --- /dev/null +++ b/example/docker-compose-mattermost/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.8" +services: + gatus: + image: twinproduction/gatus:latest + ports: + - 8080:8080 + volumes: + - ./config.yaml:/config/config.yaml + + mattermost-preview: + image: mattermost/mattermost-preview:latest + ports: + - 8065:8065 \ No newline at end of file