Add Mettermost Alerting

This commit is contained in:
Zied ZAIEM 2020-11-14 15:55:37 +01:00
parent 79c60d834e
commit 39aaae8b50
11 changed files with 190 additions and 2 deletions

BIN
.github/assets/mattermost-alerts.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -5,6 +5,10 @@ ADD . ./
RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus . RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus .
RUN apk --update add ca-certificates 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 # Run the binary on an empty container
FROM scratch FROM scratch
COPY --from=builder /app/gatus . COPY --from=builder /app/gatus .

View File

@ -24,6 +24,7 @@ core applications: https://status.twinnation.org/
- [Functions](#functions) - [Functions](#functions)
- [Alerting](#alerting) - [Alerting](#alerting)
- [Configuring Slack alerts](#configuring-slack-alerts) - [Configuring Slack alerts](#configuring-slack-alerts)
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts) - [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
- [Configuring Twilio alerts](#configuring-twilio-alerts) - [Configuring Twilio alerts](#configuring-twilio-alerts)
- [Configuring custom alerts](#configuring-custom-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. - **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. - **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. - **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** - **Metrics**
- **Low resource consumption**: As with most Go applications, the resource footprint that this application requires is negligibly small. - **Low resource consumption**: As with most Go applications, the resource footprint that this application requires is negligibly small.
- **Service auto discovery in Kubernetes** (ALPHA) - **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[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`) | `false` |
| `services[].body` | Request body | `""` | | `services[].body` | Request body | `""` |
| `services[].headers` | Request headers | `{}` | | `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[].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[].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` | | `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` | Configuration for alerting | `{}` |
| `alerting.slack` | Configuration for alerts of type `slack` | `{}` | | `alerting.slack` | Configuration for alerts of type `slack` | `{}` |
| `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` | | `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` | Configuration for alerts of type `pagerduty` | `{}` |
| `alerting.pagerduty.integration-key` | PagerDuty Events API v2 integration key. | Required `""` | | `alerting.pagerduty.integration-key` | PagerDuty Events API v2 integration key. | Required `""` |
| `alerting.twilio` | Settings for alerts of type `twilio` | `{}` | | `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) ![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 #### Configuring PagerDuty alerts
It is highly recommended to set `services[].alerts[].send-on-resolved` to `true` for alerts It is highly recommended to set `services[].alerts[].send-on-resolved` to `true` for alerts
@ -287,6 +322,7 @@ alerting:
custom: custom:
url: "https://hooks.slack.com/services/**********/**********/**********" url: "https://hooks.slack.com/services/**********/**********/**********"
method: "POST" method: "POST"
insecure: true
body: | body: |
{ {
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [SERVICE_NAME] - [ALERT_DESCRIPTION]" "text": "[ALERT_TRIGGERED_OR_RESOLVED]: [SERVICE_NAME] - [ALERT_DESCRIPTION]"

View File

@ -4,6 +4,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/alerting/provider/custom"
"github.com/TwinProduction/gatus/alerting/provider/pagerduty" "github.com/TwinProduction/gatus/alerting/provider/pagerduty"
"github.com/TwinProduction/gatus/alerting/provider/slack" "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/alerting/provider/twilio"
) )
@ -12,6 +13,9 @@ type Config struct {
// Slack is the configuration for the slack alerting provider // Slack is the configuration for the slack alerting provider
Slack *slack.AlertProvider `yaml:"slack"` 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 is the configuration for the pagerduty alerting provider
PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty"` PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty"`

View File

@ -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"},
}
}

View File

@ -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")
}
}

View File

@ -4,6 +4,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/alerting/provider/custom"
"github.com/TwinProduction/gatus/alerting/provider/pagerduty" "github.com/TwinProduction/gatus/alerting/provider/pagerduty"
"github.com/TwinProduction/gatus/alerting/provider/slack" "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/alerting/provider/twilio"
"github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/core"
) )
@ -22,5 +23,6 @@ var (
_ AlertProvider = (*custom.AlertProvider)(nil) _ AlertProvider = (*custom.AlertProvider)(nil)
_ AlertProvider = (*twilio.AlertProvider)(nil) _ AlertProvider = (*twilio.AlertProvider)(nil)
_ AlertProvider = (*slack.AlertProvider)(nil) _ AlertProvider = (*slack.AlertProvider)(nil)
_ AlertProvider = (*mattermost.AlertProvider)(nil)
_ AlertProvider = (*pagerduty.AlertProvider)(nil) _ AlertProvider = (*pagerduty.AlertProvider)(nil)
) )

View File

@ -179,6 +179,7 @@ func validateAlertingConfig(config *Config) {
} }
alertTypes := []core.AlertType{ alertTypes := []core.AlertType{
core.SlackAlert, core.SlackAlert,
core.MattermostAlert,
core.TwilioAlert, core.TwilioAlert,
core.PagerDutyAlert, core.PagerDutyAlert,
core.CustomAlert, core.CustomAlert,
@ -209,6 +210,12 @@ func GetAlertingProviderByAlertType(config *Config, alertType core.AlertType) pr
return nil return nil
} }
return config.Alerting.Slack 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: case core.TwilioAlert:
if config.Alerting.Twilio == nil { if config.Alerting.Twilio == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil // Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil

View File

@ -37,6 +37,9 @@ const (
// SlackAlert is the AlertType for the slack alerting provider // SlackAlert is the AlertType for the slack alerting provider
SlackAlert AlertType = "slack" 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 is the AlertType for the pagerduty alerting provider
PagerDutyAlert AlertType = "pagerduty" PagerDutyAlert AlertType = "pagerduty"

View File

@ -0,0 +1,6 @@
services:
- name: example
url: http://example.org
interval: 30s
conditions:
- "[STATUS] == 200"

View File

@ -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