Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bc381b356 | ||
|
|
18420c2d60 | ||
|
|
0b4dc34c57 | ||
|
|
030212c156 | ||
|
|
63b0ac8b35 | ||
|
|
263b2f0f94 | ||
|
|
db23bd9073 | ||
|
|
40dc1cc270 | ||
|
|
67c3bf6e5e | ||
|
|
57ef931d38 | ||
|
|
e3038f0e80 | ||
|
|
8106832d69 | ||
|
|
758428b312 | ||
|
|
77de4c4742 | ||
|
|
a85c5d5486 | ||
|
|
c7d554efa5 | ||
|
|
f3afdf2977 | ||
|
|
19a0ba7271 | ||
|
|
4a4c88ae17 | ||
|
|
253e6f8338 |
No files matched your search
@@ -49,6 +49,7 @@ core applications: https://status.twinnation.org/
|
||||
- [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries)
|
||||
- [Basic authentication](#basic-authentication)
|
||||
- [disable-monitoring-lock](#disable-monitoring-lock)
|
||||
- [Reloading configuration on the fly](#reloading-configuration-on-the-fly)
|
||||
- [Service groups](#service-groups)
|
||||
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
||||
- [Uptime Badges (ALPHA)](#uptime-badges)
|
||||
@@ -116,7 +117,7 @@ This example would look like this:
|
||||
|
||||

|
||||
|
||||
Note that you can also add environment variables in the configuration file (e.g. `$DOMAIN`, `${DOMAIN}`)
|
||||
Note that you can also use environment variables in the configuration file (e.g. `$DOMAIN`, `${DOMAIN}`)
|
||||
|
||||
|
||||
## Configuration
|
||||
@@ -153,6 +154,7 @@ Note that you can also add environment variables in the configuration file (e.g.
|
||||
| `security.basic.username` | Username for Basic authentication | Required `""` |
|
||||
| `security.basic.password-sha512` | Password's SHA512 hash for Basic authentication | Required `""` |
|
||||
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock) | `false` |
|
||||
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. See [Reloading configuration on the fly](#reloading-configuration-on-the-fly).
|
||||
| `web` | Web configuration | `{}` |
|
||||
| `web.address` | Address to listen on | `0.0.0.0` |
|
||||
| `web.port` | Port to listen on | `8080` |
|
||||
@@ -250,6 +252,11 @@ ignored.
|
||||
| `alerting.custom.insecure` | Whether to skip verifying the server's certificate chain and host name | `false` |
|
||||
| `alerting.custom.body` | Custom alerting request body. | `""` |
|
||||
| `alerting.custom.headers` | Custom alerting request headers | `{}` |
|
||||
| `alerting.*.default-alert.enabled` | Whether to enable the alert | N/A |
|
||||
| `alerting.*.default-alert.failure-threshold` | Number of failures in a row needed before triggering the alert | N/A |
|
||||
| `alerting.*.default-alert.success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.description` | Description of the alert. Will be included in the alert sent | N/A |
|
||||
|
||||
|
||||
#### Configuring Slack alerts
|
||||
@@ -503,6 +510,59 @@ As a result, the `[ALERT_TRIGGERED_OR_RESOLVED]` in the body of first example of
|
||||
`partial_outage` when an alert is triggered and `operational` when an alert is resolved.
|
||||
|
||||
|
||||
#### Setting a default provider alert
|
||||
|
||||
While you can specify the alert configuration directly in the service definition, it's tedious and may lead to a very
|
||||
long configuration file.
|
||||
|
||||
To avoid such problem, you can use the `default-alert` parameter present in each provider configuration:
|
||||
```yaml
|
||||
alerting:
|
||||
slack:
|
||||
webhook-url: "https://hooks.slack.com/services/**********/**********/**********"
|
||||
default-alert:
|
||||
enabled: true
|
||||
description: "healthcheck failed"
|
||||
send-on-resolved: true
|
||||
failure-threshold: 5
|
||||
success-threshold: 5
|
||||
```
|
||||
|
||||
As a result, your service configuration looks a lot tidier:
|
||||
```yaml
|
||||
services:
|
||||
- name: example
|
||||
url: "https://example.org"
|
||||
alerts:
|
||||
- type: slack
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
- name: other-example
|
||||
url: "https://example.com"
|
||||
alerts:
|
||||
- type: slack
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
```
|
||||
|
||||
It also allows you to do things like this:
|
||||
```yaml
|
||||
services:
|
||||
- name: twinnation
|
||||
url: "https://twinnation.org/health"
|
||||
alerts:
|
||||
- type: slack
|
||||
failure-threshold: 5
|
||||
- type: slack
|
||||
failure-threshold: 10
|
||||
- type: slack
|
||||
failure-threshold: 15
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
```
|
||||
|
||||
|
||||
### Kubernetes (ALPHA)
|
||||
|
||||
> **WARNING**: This feature is in ALPHA. This means that it is very likely to change in the near future, which means that
|
||||
@@ -581,6 +641,11 @@ If you're on Windows, replace `"$(pwd)"` by the absolute path to your current di
|
||||
docker run -p 8080:8080 --mount type=bind,source=C:/Users/Chris/Desktop/config.yaml,target=/config/config.yaml --name gatus twinproduction/gatus
|
||||
```
|
||||
|
||||
To build the image locally:
|
||||
```
|
||||
docker build . -t twinproduction/gatus
|
||||
```
|
||||
|
||||
|
||||
## Running the tests
|
||||
|
||||
@@ -755,6 +820,30 @@ technically, if you create 100 services with a 1 seconds interval, Gatus will se
|
||||
- You want to test multiple services at very short interval (< 5s)
|
||||
|
||||
|
||||
### Reloading configuration on the fly
|
||||
|
||||
For the sake on convenience, Gatus automatically reloads the configuration on the fly if the loaded configuration file
|
||||
is updated while Gatus is running.
|
||||
|
||||
By default, the application will exit if the updating configuration is invalid, but you can configure
|
||||
Gatus to continue running if the configuration file is updated with an invalid configuration by
|
||||
setting `skip-invalid-config-update` to `true`.
|
||||
|
||||
Keep in mind that it is in your best interest to ensure the validity of the configuration file after each update you
|
||||
apply to the configuration file while Gatus is running by looking at the log and making sure that you do not see the
|
||||
following message:
|
||||
```
|
||||
The configuration file was updated, but it is not valid. The old configuration will continue being used.
|
||||
```
|
||||
Failure to do so may result in Gatus being unable to start if the application is restarted for whatever reason.
|
||||
|
||||
I recommend not setting `skip-invalid-config-update` to `true` to avoid a situation like this, but the choice is yours
|
||||
to make.
|
||||
|
||||
Note that if you are not using a file storage, updating the configuration while Gatus is running is effectively
|
||||
the same as restarting the application.
|
||||
|
||||
|
||||
### Service groups
|
||||
|
||||
Service groups are used for grouping multiple services together on the dashboard.
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
package core
|
||||
package alert
|
||||
|
||||
// Alert is the service's alert configuration
|
||||
type Alert struct {
|
||||
// Type of alert
|
||||
Type AlertType `yaml:"type"`
|
||||
// Type of alert (required)
|
||||
Type Type `yaml:"type"`
|
||||
|
||||
// Enabled defines whether or not the alert is enabled
|
||||
Enabled bool `yaml:"enabled"`
|
||||
//
|
||||
// This is a pointer, because it is populated by YAML and we need to know whether it was explicitly set to a value
|
||||
// or not for provider.ParseWithDefaultAlert to work.
|
||||
Enabled *bool `yaml:"enabled"`
|
||||
|
||||
// FailureThreshold is the number of failures in a row needed before triggering the alert
|
||||
FailureThreshold int `yaml:"failure-threshold"`
|
||||
|
||||
// Description of the alert. Will be included in the alert sent.
|
||||
Description string `yaml:"description"`
|
||||
//
|
||||
// This is a pointer, because it is populated by YAML and we need to know whether it was explicitly set to a value
|
||||
// or not for provider.ParseWithDefaultAlert to work.
|
||||
Description *string `yaml:"description"`
|
||||
|
||||
// SendOnResolved defines whether to send a second notification when the issue has been resolved
|
||||
SendOnResolved bool `yaml:"send-on-resolved"`
|
||||
//
|
||||
// This is a pointer, because it is populated by YAML and we need to know whether it was explicitly set to a value
|
||||
// or not for provider.ParseWithDefaultAlert to work. Use Alert.IsSendingOnResolved() for a non-pointer
|
||||
SendOnResolved *bool `yaml:"send-on-resolved"`
|
||||
|
||||
// SuccessThreshold defines how many successful executions must happen in a row before an ongoing incident is marked as resolved
|
||||
SuccessThreshold int `yaml:"success-threshold"`
|
||||
@@ -35,32 +44,26 @@ type Alert struct {
|
||||
Triggered bool
|
||||
}
|
||||
|
||||
// AlertType is the type of the alert.
|
||||
// The value will generally be the name of the alert provider
|
||||
type AlertType string
|
||||
// GetDescription retrieves the description of the alert
|
||||
func (alert Alert) GetDescription() string {
|
||||
if alert.Description == nil {
|
||||
return ""
|
||||
}
|
||||
return *alert.Description
|
||||
}
|
||||
|
||||
const (
|
||||
// CustomAlert is the AlertType for the custom alerting provider
|
||||
CustomAlert AlertType = "custom"
|
||||
// IsEnabled returns whether an alert is enabled or not
|
||||
func (alert Alert) IsEnabled() bool {
|
||||
if alert.Enabled == nil {
|
||||
return false
|
||||
}
|
||||
return *alert.Enabled
|
||||
}
|
||||
|
||||
// DiscordAlert is the AlertType for the discord alerting provider
|
||||
DiscordAlert AlertType = "discord"
|
||||
|
||||
// MattermostAlert is the AlertType for the mattermost alerting provider
|
||||
MattermostAlert AlertType = "mattermost"
|
||||
|
||||
// MessagebirdAlert is the AlertType for the messagebird alerting provider
|
||||
MessagebirdAlert AlertType = "messagebird"
|
||||
|
||||
// PagerDutyAlert is the AlertType for the pagerduty alerting provider
|
||||
PagerDutyAlert AlertType = "pagerduty"
|
||||
|
||||
// SlackAlert is the AlertType for the slack alerting provider
|
||||
SlackAlert AlertType = "slack"
|
||||
|
||||
// TelegramAlert is the AlertType for the telegram alerting provider
|
||||
TelegramAlert AlertType = "telegram"
|
||||
|
||||
// TwilioAlert is the AlertType for the twilio alerting provider
|
||||
TwilioAlert AlertType = "twilio"
|
||||
)
|
||||
// IsSendingOnResolved returns whether an alert is sending on resolve or not
|
||||
func (alert Alert) IsSendingOnResolved() bool {
|
||||
if alert.SendOnResolved == nil {
|
||||
return false
|
||||
}
|
||||
return *alert.SendOnResolved
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package alert
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAlert_IsEnabled(t *testing.T) {
|
||||
if (Alert{Enabled: nil}).IsEnabled() {
|
||||
t.Error("alert.IsEnabled() should've returned false, because Enabled was set to nil")
|
||||
}
|
||||
if value := false; (Alert{Enabled: &value}).IsEnabled() {
|
||||
t.Error("alert.IsEnabled() should've returned false, because Enabled was set to false")
|
||||
}
|
||||
if value := true; !(Alert{Enabled: &value}).IsEnabled() {
|
||||
t.Error("alert.IsEnabled() should've returned true, because Enabled was set to true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_GetDescription(t *testing.T) {
|
||||
if (Alert{Description: nil}).GetDescription() != "" {
|
||||
t.Error("alert.GetDescription() should've returned an empty string, because Description was set to nil")
|
||||
}
|
||||
if value := "description"; (Alert{Description: &value}).GetDescription() != value {
|
||||
t.Error("alert.GetDescription() should've returned false, because Description was set to 'description'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_IsSendingOnResolved(t *testing.T) {
|
||||
if (Alert{SendOnResolved: nil}).IsSendingOnResolved() {
|
||||
t.Error("alert.IsSendingOnResolved() should've returned false, because SendOnResolved was set to nil")
|
||||
}
|
||||
if value := false; (Alert{SendOnResolved: &value}).IsSendingOnResolved() {
|
||||
t.Error("alert.IsSendingOnResolved() should've returned false, because SendOnResolved was set to false")
|
||||
}
|
||||
if value := true; !(Alert{SendOnResolved: &value}).IsSendingOnResolved() {
|
||||
t.Error("alert.IsSendingOnResolved() should've returned true, because SendOnResolved was set to true")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package alert
|
||||
|
||||
// Type is the type of the alert.
|
||||
// The value will generally be the name of the alert provider
|
||||
type Type string
|
||||
|
||||
const (
|
||||
// TypeCustom is the Type for the custom alerting provider
|
||||
TypeCustom Type = "custom"
|
||||
|
||||
// TypeDiscord is the Type for the discord alerting provider
|
||||
TypeDiscord Type = "discord"
|
||||
|
||||
// TypeMattermost is the Type for the mattermost alerting provider
|
||||
TypeMattermost Type = "mattermost"
|
||||
|
||||
// TypeMessagebird is the Type for the messagebird alerting provider
|
||||
TypeMessagebird Type = "messagebird"
|
||||
|
||||
// TypePagerDuty is the Type for the pagerduty alerting provider
|
||||
TypePagerDuty Type = "pagerduty"
|
||||
|
||||
// TypeSlack is the Type for the slack alerting provider
|
||||
TypeSlack Type = "slack"
|
||||
|
||||
// TypeTelegram is the Type for the telegram alerting provider
|
||||
TypeTelegram Type = "telegram"
|
||||
|
||||
// TypeTwilio is the Type for the twilio alerting provider
|
||||
TypeTwilio Type = "twilio"
|
||||
)
|
||||
@@ -1,6 +1,8 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/discord"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/mattermost"
|
||||
@@ -37,3 +39,58 @@ type Config struct {
|
||||
// Twilio is the configuration for the twilio alerting provider
|
||||
Twilio *twilio.AlertProvider `yaml:"twilio"`
|
||||
}
|
||||
|
||||
// GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding alert.Type
|
||||
func (config Config) GetAlertingProviderByAlertType(alertType alert.Type) provider.AlertProvider {
|
||||
switch alertType {
|
||||
case alert.TypeCustom:
|
||||
if config.Custom == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
return nil
|
||||
}
|
||||
return config.Custom
|
||||
case alert.TypeDiscord:
|
||||
if config.Discord == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
return nil
|
||||
}
|
||||
return config.Discord
|
||||
case alert.TypeMattermost:
|
||||
if config.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.Mattermost
|
||||
case alert.TypeMessagebird:
|
||||
if config.Messagebird == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
return nil
|
||||
}
|
||||
return config.Messagebird
|
||||
case alert.TypePagerDuty:
|
||||
if config.PagerDuty == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
return nil
|
||||
}
|
||||
return config.PagerDuty
|
||||
case alert.TypeSlack:
|
||||
if config.Slack == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
return nil
|
||||
}
|
||||
return config.Slack
|
||||
case alert.TypeTelegram:
|
||||
if config.Telegram == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
return nil
|
||||
}
|
||||
return config.Telegram
|
||||
case alert.TypeTwilio:
|
||||
if config.Twilio == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
return nil
|
||||
}
|
||||
return config.Twilio
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/client"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
@@ -22,6 +23,9 @@ type AlertProvider struct {
|
||||
Body string `yaml:"body,omitempty"`
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
Placeholders map[string]map[string]string `yaml:"placeholders,omitempty"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -30,7 +34,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, result *core.Result, resolved bool) *AlertProvider {
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *alert.Alert, result *core.Result, resolved bool) *AlertProvider {
|
||||
return provider
|
||||
}
|
||||
|
||||
@@ -112,3 +116,8 @@ func (provider *AlertProvider) Send(serviceName, alertDescription string, resolv
|
||||
}
|
||||
return ioutil.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
@@ -60,7 +61,7 @@ func TestAlertProvider_buildHTTPRequestWhenTriggered(t *testing.T) {
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProvider(t *testing.T) {
|
||||
provider := AlertProvider{URL: "http://example.com"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{}, true)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
@@ -11,6 +12,9 @@ import (
|
||||
// AlertProvider is the configuration necessary for sending an alert using Discord
|
||||
type AlertProvider struct {
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -19,7 +23,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||
var message, results string
|
||||
var colorCode int
|
||||
if resolved {
|
||||
@@ -57,7 +61,12 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, message, alert.Description, colorCode, results),
|
||||
}`, message, alert.GetDescription(), colorCode, results),
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
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)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
@@ -44,7 +45,7 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
|
||||
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)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
@@ -12,6 +13,9 @@ import (
|
||||
type AlertProvider struct {
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
Insecure bool `yaml:"insecure,omitempty"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -20,7 +24,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||
var message string
|
||||
var color string
|
||||
if resolved {
|
||||
@@ -69,7 +73,12 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, message, message, alert.Description, color, service.URL, results),
|
||||
}`, message, message, alert.GetDescription(), color, service.URL, results),
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.org"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
@@ -44,7 +45,7 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.org"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
@@ -17,6 +18,9 @@ type AlertProvider struct {
|
||||
AccessKey string `yaml:"access-key"`
|
||||
Originator string `yaml:"originator"`
|
||||
Recipients string `yaml:"recipients"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -26,12 +30,12 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
// Reference doc for messagebird https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, _ *core.Result, resolved bool) *custom.AlertProvider {
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *alert.Alert, _ *core.Result, resolved bool) *custom.AlertProvider {
|
||||
var message string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.Description)
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.GetDescription())
|
||||
} else {
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", service.Name, alert.Description)
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", service.Name, alert.GetDescription())
|
||||
}
|
||||
|
||||
return &custom.AlertProvider{
|
||||
@@ -48,3 +52,8 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
@@ -30,7 +31,7 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
Originator: "1",
|
||||
Recipients: "1",
|
||||
}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{}, true)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
@@ -56,7 +57,7 @@ func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
Originator: "1",
|
||||
Recipients: "1",
|
||||
}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{}, false)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
@@ -11,6 +12,9 @@ import (
|
||||
// AlertProvider is the configuration necessary for sending an alert using PagerDuty
|
||||
type AlertProvider struct {
|
||||
IntegrationKey string `yaml:"integration-key"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -21,14 +25,14 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
//
|
||||
// relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, _ *core.Result, resolved bool) *custom.AlertProvider {
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *alert.Alert, _ *core.Result, resolved bool) *custom.AlertProvider {
|
||||
var message, eventAction, resolveKey string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.Description)
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.GetDescription())
|
||||
eventAction = "resolve"
|
||||
resolveKey = alert.ResolveKey
|
||||
} else {
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", service.Name, alert.Description)
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", service.Name, alert.GetDescription())
|
||||
eventAction = "trigger"
|
||||
resolveKey = ""
|
||||
}
|
||||
@@ -50,3 +54,8 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{IntegrationKey: "00000000000000000000000000000000"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{}, true)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
@@ -44,7 +45,7 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{IntegrationKey: "00000000000000000000000000000000"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{}, false)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/discord"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/mattermost"
|
||||
@@ -18,7 +19,32 @@ type AlertProvider interface {
|
||||
IsValid() bool
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
ToCustomAlertProvider(service *core.Service, alert *core.Alert, result *core.Result, resolved bool) *custom.AlertProvider
|
||||
ToCustomAlertProvider(service *core.Service, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
GetDefaultAlert() *alert.Alert
|
||||
}
|
||||
|
||||
// ParseWithDefaultAlert parses a service alert by using the provider's default alert as a baseline
|
||||
func ParseWithDefaultAlert(providerDefaultAlert, serviceAlert *alert.Alert) {
|
||||
if providerDefaultAlert == nil || serviceAlert == nil {
|
||||
return
|
||||
}
|
||||
if serviceAlert.Enabled == nil {
|
||||
serviceAlert.Enabled = providerDefaultAlert.Enabled
|
||||
}
|
||||
if serviceAlert.SendOnResolved == nil {
|
||||
serviceAlert.SendOnResolved = providerDefaultAlert.SendOnResolved
|
||||
}
|
||||
if serviceAlert.Description == nil {
|
||||
serviceAlert.Description = providerDefaultAlert.Description
|
||||
}
|
||||
if serviceAlert.FailureThreshold == 0 {
|
||||
serviceAlert.FailureThreshold = providerDefaultAlert.FailureThreshold
|
||||
}
|
||||
if serviceAlert.SuccessThreshold == 0 {
|
||||
serviceAlert.SuccessThreshold = providerDefaultAlert.SuccessThreshold
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
)
|
||||
|
||||
func TestParseWithDefaultAlert(t *testing.T) {
|
||||
type Scenario struct {
|
||||
Name string
|
||||
DefaultAlert, ServiceAlert, ExpectedOutputAlert *alert.Alert
|
||||
}
|
||||
enabled := true
|
||||
disabled := false
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []Scenario{
|
||||
{
|
||||
Name: "service-alert-type-only",
|
||||
DefaultAlert: &alert.Alert{
|
||||
Enabled: &enabled,
|
||||
SendOnResolved: &enabled,
|
||||
Description: &firstDescription,
|
||||
FailureThreshold: 5,
|
||||
SuccessThreshold: 10,
|
||||
},
|
||||
ServiceAlert: &alert.Alert{
|
||||
Type: alert.TypeDiscord,
|
||||
},
|
||||
ExpectedOutputAlert: &alert.Alert{
|
||||
Type: alert.TypeDiscord,
|
||||
Enabled: &enabled,
|
||||
SendOnResolved: &enabled,
|
||||
Description: &firstDescription,
|
||||
FailureThreshold: 5,
|
||||
SuccessThreshold: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "service-alert-overwrites-default-alert",
|
||||
DefaultAlert: &alert.Alert{
|
||||
Enabled: &disabled,
|
||||
SendOnResolved: &disabled,
|
||||
Description: &firstDescription,
|
||||
FailureThreshold: 5,
|
||||
SuccessThreshold: 10,
|
||||
},
|
||||
ServiceAlert: &alert.Alert{
|
||||
Type: alert.TypeTelegram,
|
||||
Enabled: &enabled,
|
||||
SendOnResolved: &enabled,
|
||||
Description: &secondDescription,
|
||||
FailureThreshold: 6,
|
||||
SuccessThreshold: 11,
|
||||
},
|
||||
ExpectedOutputAlert: &alert.Alert{
|
||||
Type: alert.TypeTelegram,
|
||||
Enabled: &enabled,
|
||||
SendOnResolved: &enabled,
|
||||
Description: &secondDescription,
|
||||
FailureThreshold: 6,
|
||||
SuccessThreshold: 11,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "service-alert-partially-overwrites-default-alert",
|
||||
DefaultAlert: &alert.Alert{
|
||||
Enabled: &enabled,
|
||||
SendOnResolved: &enabled,
|
||||
Description: &firstDescription,
|
||||
FailureThreshold: 5,
|
||||
SuccessThreshold: 10,
|
||||
},
|
||||
ServiceAlert: &alert.Alert{
|
||||
Type: alert.TypeDiscord,
|
||||
Enabled: nil,
|
||||
SendOnResolved: nil,
|
||||
FailureThreshold: 6,
|
||||
SuccessThreshold: 11,
|
||||
},
|
||||
ExpectedOutputAlert: &alert.Alert{
|
||||
Type: alert.TypeDiscord,
|
||||
Enabled: &enabled,
|
||||
SendOnResolved: &enabled,
|
||||
Description: &firstDescription,
|
||||
FailureThreshold: 6,
|
||||
SuccessThreshold: 11,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "default-alert-type-should-be-ignored",
|
||||
DefaultAlert: &alert.Alert{
|
||||
Type: alert.TypeTelegram,
|
||||
Enabled: &enabled,
|
||||
SendOnResolved: &enabled,
|
||||
Description: &firstDescription,
|
||||
FailureThreshold: 5,
|
||||
SuccessThreshold: 10,
|
||||
},
|
||||
ServiceAlert: &alert.Alert{
|
||||
Type: alert.TypeDiscord,
|
||||
},
|
||||
ExpectedOutputAlert: &alert.Alert{
|
||||
Type: alert.TypeDiscord,
|
||||
Enabled: &enabled,
|
||||
SendOnResolved: &enabled,
|
||||
Description: &firstDescription,
|
||||
FailureThreshold: 5,
|
||||
SuccessThreshold: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "no-default-alert",
|
||||
DefaultAlert: &alert.Alert{
|
||||
Type: alert.TypeDiscord,
|
||||
Enabled: nil,
|
||||
SendOnResolved: nil,
|
||||
Description: &firstDescription,
|
||||
FailureThreshold: 2,
|
||||
SuccessThreshold: 5,
|
||||
},
|
||||
ServiceAlert: nil,
|
||||
ExpectedOutputAlert: nil,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
ParseWithDefaultAlert(scenario.DefaultAlert, scenario.ServiceAlert)
|
||||
if scenario.ExpectedOutputAlert == nil {
|
||||
if scenario.ServiceAlert != nil {
|
||||
t.Fail()
|
||||
}
|
||||
return
|
||||
}
|
||||
if scenario.ServiceAlert.IsEnabled() != scenario.ExpectedOutputAlert.IsEnabled() {
|
||||
t.Errorf("expected ServiceAlert.IsEnabled() to be %v, got %v", scenario.ExpectedOutputAlert.IsEnabled(), scenario.ServiceAlert.IsEnabled())
|
||||
}
|
||||
if scenario.ServiceAlert.IsSendingOnResolved() != scenario.ExpectedOutputAlert.IsSendingOnResolved() {
|
||||
t.Errorf("expected ServiceAlert.IsSendingOnResolved() to be %v, got %v", scenario.ExpectedOutputAlert.IsSendingOnResolved(), scenario.ServiceAlert.IsSendingOnResolved())
|
||||
}
|
||||
if scenario.ServiceAlert.GetDescription() != scenario.ExpectedOutputAlert.GetDescription() {
|
||||
t.Errorf("expected ServiceAlert.GetDescription() to be %v, got %v", scenario.ExpectedOutputAlert.GetDescription(), scenario.ServiceAlert.GetDescription())
|
||||
}
|
||||
if scenario.ServiceAlert.FailureThreshold != scenario.ExpectedOutputAlert.FailureThreshold {
|
||||
t.Errorf("expected ServiceAlert.FailureThreshold to be %v, got %v", scenario.ExpectedOutputAlert.FailureThreshold, scenario.ServiceAlert.FailureThreshold)
|
||||
}
|
||||
if scenario.ServiceAlert.SuccessThreshold != scenario.ExpectedOutputAlert.SuccessThreshold {
|
||||
t.Errorf("expected ServiceAlert.SuccessThreshold to be %v, got %v", scenario.ExpectedOutputAlert.SuccessThreshold, scenario.ServiceAlert.SuccessThreshold)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
@@ -11,6 +12,9 @@ import (
|
||||
// AlertProvider is the configuration necessary for sending an alert using Slack
|
||||
type AlertProvider struct {
|
||||
WebhookURL string `yaml:"webhook-url"` // Slack webhook URL
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -19,7 +23,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||
var message, color, results 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)
|
||||
@@ -57,7 +61,12 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, message, alert.Description, color, results),
|
||||
}`, message, alert.GetDescription(), color, results),
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
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)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
@@ -44,7 +45,7 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
|
||||
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)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
@@ -12,6 +13,9 @@ import (
|
||||
type AlertProvider struct {
|
||||
Token string `yaml:"token"`
|
||||
ID string `yaml:"id"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -20,7 +24,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||
var message, results string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved:\\n—\\n _healthcheck passing successfully %d time(s) in a row_\\n— ", service.Name, alert.FailureThreshold)
|
||||
@@ -37,8 +41,8 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
|
||||
}
|
||||
var text string
|
||||
if len(alert.Description) > 0 {
|
||||
text = fmt.Sprintf("⛑ *Gatus* \\n%s \\n*Description* \\n_%s_ \\n\\n*Condition results*\\n%s", message, alert.Description, results)
|
||||
if len(alert.GetDescription()) > 0 {
|
||||
text = fmt.Sprintf("⛑ *Gatus* \\n%s \\n*Description* \\n_%s_ \\n\\n*Condition results*\\n%s", message, alert.GetDescription(), results)
|
||||
} else {
|
||||
text = fmt.Sprintf("⛑ *Gatus* \\n%s \\n*Condition results*\\n%s", message, results)
|
||||
}
|
||||
@@ -49,3 +53,8 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
@@ -23,7 +24,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
@@ -46,7 +47,8 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{Description: "Healthcheck Successful"}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
description := "Healthcheck Successful"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{Description: &description}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
@@ -68,7 +70,7 @@ func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithDescription(t *testing.T) {
|
||||
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
@@ -16,6 +17,9 @@ type AlertProvider struct {
|
||||
Token string `yaml:"token"`
|
||||
From string `yaml:"from"`
|
||||
To string `yaml:"to"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -24,12 +28,12 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, _ *core.Result, resolved bool) *custom.AlertProvider {
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *alert.Alert, _ *core.Result, resolved bool) *custom.AlertProvider {
|
||||
var message string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.Description)
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.GetDescription())
|
||||
} else {
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", service.Name, alert.Description)
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", service.Name, alert.GetDescription())
|
||||
}
|
||||
return &custom.AlertProvider{
|
||||
URL: fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID),
|
||||
@@ -45,3 +49,8 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
@@ -31,7 +32,8 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
From: "3",
|
||||
To: "4",
|
||||
}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{Name: "service-name"}, &core.Alert{Description: "alert-description"}, &core.Result{}, true)
|
||||
description := "alert-description"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{Name: "service-name"}, &alert.Alert{Description: &description}, &core.Result{}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
@@ -56,7 +58,8 @@ func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
From: "2",
|
||||
To: "1",
|
||||
}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{Name: "service-name"}, &core.Alert{Description: "alert-description"}, &core.Result{}, false)
|
||||
description := "alert-description"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{Name: "service-name"}, &alert.Alert{Description: &description}, &core.Result{}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ func Ping(address string) (bool, time.Duration) {
|
||||
}
|
||||
pinger.Count = 1
|
||||
pinger.Timeout = pingTimeout
|
||||
pinger.SetNetwork("ip4")
|
||||
pinger.SetPrivileged(true)
|
||||
err = pinger.Run()
|
||||
if err != nil {
|
||||
|
||||
+7
-1
@@ -45,4 +45,10 @@ services:
|
||||
query-type: "A"
|
||||
conditions:
|
||||
- "[BODY] == 93.184.216.34"
|
||||
- "[DNS_RCODE] == NOERROR"
|
||||
- "[DNS_RCODE] == NOERROR"
|
||||
|
||||
- name: icmp-ping
|
||||
url: "icmp://example.org"
|
||||
interval: 1m
|
||||
conditions:
|
||||
- "[CONNECTED] == true"
|
||||
+100
-108
@@ -5,8 +5,10 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting"
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
"github.com/TwinProduction/gatus/k8s"
|
||||
@@ -39,13 +41,8 @@ var (
|
||||
// ErrConfigFileNotFound is an error returned when the configuration file could not be found
|
||||
ErrConfigFileNotFound = errors.New("configuration file not found")
|
||||
|
||||
// ErrConfigNotLoaded is an error returned when an attempt to Get() the configuration before loading it is made
|
||||
ErrConfigNotLoaded = errors.New("configuration is nil")
|
||||
|
||||
// ErrInvalidSecurityConfig is an error returned when the security configuration is invalid
|
||||
ErrInvalidSecurityConfig = errors.New("invalid security configuration")
|
||||
|
||||
config *Config
|
||||
)
|
||||
|
||||
// Config is the main configuration structure
|
||||
@@ -56,6 +53,10 @@ type Config struct {
|
||||
// Metrics Whether to expose metrics at /metrics
|
||||
Metrics bool `yaml:"metrics"`
|
||||
|
||||
// SkipInvalidConfigUpdate Whether to make the application ignore invalid configuration
|
||||
// if the configuration file is updated while the application is running
|
||||
SkipInvalidConfigUpdate bool `yaml:"skip-invalid-config-update"`
|
||||
|
||||
// DisableMonitoringLock Whether to disable the monitoring lock
|
||||
// The monitoring lock is what prevents multiple services from being processed at the same time.
|
||||
// Disabling this may lead to inaccurate response times
|
||||
@@ -78,47 +79,57 @@ type Config struct {
|
||||
|
||||
// Web is the configuration for the web listener
|
||||
Web *WebConfig `yaml:"web"`
|
||||
|
||||
filePath string // path to the file from which config was loaded from
|
||||
lastFileModTime time.Time // last modification time
|
||||
}
|
||||
|
||||
// Get returns the configuration, or panics if the configuration hasn't loaded yet
|
||||
func Get() *Config {
|
||||
if config == nil {
|
||||
panic(ErrConfigNotLoaded)
|
||||
// HasLoadedConfigurationFileBeenModified returns whether the file that the
|
||||
// configuration has been loaded from has been modified since it was last read
|
||||
func (config Config) HasLoadedConfigurationFileBeenModified() bool {
|
||||
if fileInfo, err := os.Stat(config.filePath); err == nil {
|
||||
if !fileInfo.ModTime().IsZero() {
|
||||
return config.lastFileModTime.Unix() != fileInfo.ModTime().Unix()
|
||||
}
|
||||
}
|
||||
return config
|
||||
return false
|
||||
}
|
||||
|
||||
// Set sets the configuration
|
||||
// Used only for testing
|
||||
func Set(cfg *Config) {
|
||||
config = cfg
|
||||
// UpdateLastFileModTime refreshes Config.lastFileModTime
|
||||
func (config *Config) UpdateLastFileModTime() {
|
||||
if fileInfo, err := os.Stat(config.filePath); err == nil {
|
||||
if !fileInfo.ModTime().IsZero() {
|
||||
config.lastFileModTime = fileInfo.ModTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads a custom configuration file
|
||||
// Note that the misconfiguration of some fields may lead to panics. This is on purpose.
|
||||
func Load(configFile string) error {
|
||||
func Load(configFile string) (*Config, error) {
|
||||
log.Printf("[config][Load] Reading configuration from configFile=%s", configFile)
|
||||
cfg, err := readConfigurationFile(configFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return ErrConfigFileNotFound
|
||||
return nil, ErrConfigFileNotFound
|
||||
}
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
config = cfg
|
||||
return nil
|
||||
cfg.filePath = configFile
|
||||
cfg.UpdateLastFileModTime()
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// LoadDefaultConfiguration loads the default configuration file
|
||||
func LoadDefaultConfiguration() error {
|
||||
err := Load(DefaultConfigurationFilePath)
|
||||
func LoadDefaultConfiguration() (*Config, error) {
|
||||
cfg, err := Load(DefaultConfigurationFilePath)
|
||||
if err != nil {
|
||||
if err == ErrConfigFileNotFound {
|
||||
return Load(DefaultFallbackConfigurationFilePath)
|
||||
}
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func readConfigurationFile(fileName string) (config *Config, err error) {
|
||||
@@ -144,23 +155,33 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
||||
} else {
|
||||
// Note that the functions below may panic, and this is on purpose to prevent Gatus from starting with
|
||||
// invalid configurations
|
||||
validateAlertingConfig(config)
|
||||
validateSecurityConfig(config)
|
||||
validateServicesConfig(config)
|
||||
validateKubernetesConfig(config)
|
||||
validateWebConfig(config)
|
||||
validateStorageConfig(config)
|
||||
validateAlertingConfig(config.Alerting, config.Services, config.Debug)
|
||||
if err := validateSecurityConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateServicesConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateKubernetesConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateWebConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateStorageConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func validateStorageConfig(config *Config) {
|
||||
func validateStorageConfig(config *Config) error {
|
||||
if config.Storage == nil {
|
||||
config.Storage = &storage.Config{}
|
||||
}
|
||||
err := storage.Initialize(config.Storage)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
// Remove all ServiceStatus that represent services which no longer exist in the configuration
|
||||
var keys []string
|
||||
@@ -171,44 +192,52 @@ func validateStorageConfig(config *Config) {
|
||||
if numberOfServiceStatusesDeleted > 0 {
|
||||
log.Printf("[config][validateStorageConfig] Deleted %d service statuses because their matching services no longer existed", numberOfServiceStatusesDeleted)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateWebConfig(config *Config) {
|
||||
func validateWebConfig(config *Config) error {
|
||||
if config.Web == nil {
|
||||
config.Web = &WebConfig{Address: DefaultAddress, Port: DefaultPort}
|
||||
} else {
|
||||
config.Web.validateAndSetDefaults()
|
||||
return config.Web.validateAndSetDefaults()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateKubernetesConfig(config *Config) {
|
||||
// deprecated
|
||||
// I don't like the current implementation.
|
||||
func validateKubernetesConfig(config *Config) error {
|
||||
if config.Kubernetes != nil && config.Kubernetes.AutoDiscover {
|
||||
if config.Kubernetes.ServiceTemplate == nil {
|
||||
panic("kubernetes.service-template cannot be nil")
|
||||
return errors.New("kubernetes.service-template cannot be nil")
|
||||
}
|
||||
if config.Debug {
|
||||
log.Println("[config][validateKubernetesConfig] Automatically discovering Kubernetes services...")
|
||||
}
|
||||
discoveredServices, err := k8s.DiscoverServices(config.Kubernetes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
config.Services = append(config.Services, discoveredServices...)
|
||||
log.Printf("[config][validateKubernetesConfig] Discovered %d Kubernetes services", len(discoveredServices))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateServicesConfig(config *Config) {
|
||||
func validateServicesConfig(config *Config) error {
|
||||
for _, service := range config.Services {
|
||||
if config.Debug {
|
||||
log.Printf("[config][validateServicesConfig] Validating service '%s'", service.Name)
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
if err := service.ValidateAndSetDefaults(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Printf("[config][validateServicesConfig] Validated %d services", len(config.Services))
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSecurityConfig(config *Config) {
|
||||
func validateSecurityConfig(config *Config) error {
|
||||
if config.Security != nil {
|
||||
if config.Security.IsValid() {
|
||||
if config.Debug {
|
||||
@@ -217,31 +246,49 @@ func validateSecurityConfig(config *Config) {
|
||||
} else {
|
||||
// If there was an attempt to configure security, then it must mean that some confidential or private
|
||||
// data are exposed. As a result, we'll force a panic because it's better to be safe than sorry.
|
||||
panic(ErrInvalidSecurityConfig)
|
||||
return ErrInvalidSecurityConfig
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAlertingConfig(config *Config) {
|
||||
if config.Alerting == nil {
|
||||
// validateAlertingConfig validates the alerting configuration
|
||||
// Note that the alerting configuration has to be validated before the service configuration, because the default alert
|
||||
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before core.Service.ValidateAndSetDefaults()
|
||||
// sets the default alert values when none are set.
|
||||
func validateAlertingConfig(alertingConfig *alerting.Config, services []*core.Service, debug bool) {
|
||||
if alertingConfig == nil {
|
||||
log.Printf("[config][validateAlertingConfig] Alerting is not configured")
|
||||
return
|
||||
}
|
||||
alertTypes := []core.AlertType{
|
||||
core.CustomAlert,
|
||||
core.DiscordAlert,
|
||||
core.MattermostAlert,
|
||||
core.MessagebirdAlert,
|
||||
core.PagerDutyAlert,
|
||||
core.SlackAlert,
|
||||
core.TelegramAlert,
|
||||
core.TwilioAlert,
|
||||
alertTypes := []alert.Type{
|
||||
alert.TypeCustom,
|
||||
alert.TypeDiscord,
|
||||
alert.TypeMattermost,
|
||||
alert.TypeMessagebird,
|
||||
alert.TypePagerDuty,
|
||||
alert.TypeSlack,
|
||||
alert.TypeTelegram,
|
||||
alert.TypeTwilio,
|
||||
}
|
||||
var validProviders, invalidProviders []core.AlertType
|
||||
var validProviders, invalidProviders []alert.Type
|
||||
for _, alertType := range alertTypes {
|
||||
alertProvider := GetAlertingProviderByAlertType(config, alertType)
|
||||
alertProvider := alertingConfig.GetAlertingProviderByAlertType(alertType)
|
||||
if alertProvider != nil {
|
||||
if alertProvider.IsValid() {
|
||||
// Parse alerts with the provider's default alert
|
||||
if alertProvider.GetDefaultAlert() != nil {
|
||||
for _, service := range services {
|
||||
for alertIndex, serviceAlert := range service.Alerts {
|
||||
if alertType == serviceAlert.Type {
|
||||
if debug {
|
||||
log.Printf("[config][validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in service=%s", alertIndex, alertType, service.Name)
|
||||
}
|
||||
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), serviceAlert)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
validProviders = append(validProviders, alertType)
|
||||
} else {
|
||||
log.Printf("[config][validateAlertingConfig] Ignoring provider=%s because configuration is invalid", alertType)
|
||||
@@ -253,58 +300,3 @@ func validateAlertingConfig(config *Config) {
|
||||
}
|
||||
log.Printf("[config][validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders)
|
||||
}
|
||||
|
||||
// GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding core.AlertType
|
||||
func GetAlertingProviderByAlertType(config *Config, alertType core.AlertType) provider.AlertProvider {
|
||||
switch alertType {
|
||||
case core.CustomAlert:
|
||||
if config.Alerting.Custom == 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.Custom
|
||||
case core.DiscordAlert:
|
||||
if config.Alerting.Discord == 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.Discord
|
||||
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.MessagebirdAlert:
|
||||
if config.Alerting.Messagebird == 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.Messagebird
|
||||
case core.PagerDutyAlert:
|
||||
if config.Alerting.PagerDuty == 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.PagerDuty
|
||||
case core.SlackAlert:
|
||||
if config.Alerting.Slack == 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.Slack
|
||||
case core.TelegramAlert:
|
||||
if config.Alerting.Telegram == 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.Telegram
|
||||
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
|
||||
return nil
|
||||
}
|
||||
return config.Alerting.Twilio
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+408
-72
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting"
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/discord"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/mattermost"
|
||||
@@ -20,31 +21,15 @@ import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestGetBeforeConfigIsLoaded(t *testing.T) {
|
||||
defer func() { recover() }()
|
||||
Get()
|
||||
t.Fatal("Should've panicked because the configuration hasn't been loaded yet")
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
if config != nil {
|
||||
t.Fatal("config should've been nil")
|
||||
}
|
||||
Set(&Config{})
|
||||
if config == nil {
|
||||
t.Fatal("config shouldn't have been nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileThatDoesNotExist(t *testing.T) {
|
||||
err := Load("file-that-does-not-exist.yaml")
|
||||
_, err := Load("file-that-does-not-exist.yaml")
|
||||
if err == nil {
|
||||
t.Error("Should've returned an error, because the file specified doesn't exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDefaultConfigurationFile(t *testing.T) {
|
||||
err := LoadDefaultConfiguration()
|
||||
_, err := LoadDefaultConfiguration()
|
||||
if err == nil {
|
||||
t.Error("Should've returned an error, because there's no configuration files at the default path nor the default fallback path")
|
||||
}
|
||||
@@ -236,8 +221,7 @@ services:
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithInvalidPort(t *testing.T) {
|
||||
defer func() { recover() }()
|
||||
_, _ = parseAndValidateConfigBytes([]byte(`
|
||||
_, err := parseAndValidateConfigBytes([]byte(`
|
||||
web:
|
||||
port: 65536
|
||||
address: 127.0.0.1
|
||||
@@ -247,7 +231,9 @@ services:
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
`))
|
||||
t.Fatal("Should've panicked because the configuration specifies an invalid port value")
|
||||
if err == nil {
|
||||
t.Fatal("Should've returned an error because the configuration specifies an invalid port value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithMetricsAndCustomUserAgentHeader(t *testing.T) {
|
||||
@@ -351,6 +337,8 @@ alerting:
|
||||
webhook-url: "http://example.org"
|
||||
pagerduty:
|
||||
integration-key: "00000000000000000000000000000000"
|
||||
mattermost:
|
||||
webhook-url: "http://example.com"
|
||||
messagebird:
|
||||
access-key: "1"
|
||||
originator: "31619191918"
|
||||
@@ -358,6 +346,12 @@ alerting:
|
||||
telegram:
|
||||
token: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
|
||||
id: 0123456789
|
||||
twilio:
|
||||
sid: "1234"
|
||||
token: "5678"
|
||||
from: "+1-234-567-8901"
|
||||
to: "+1-234-567-8901"
|
||||
|
||||
services:
|
||||
- name: twinnation
|
||||
url: https://twinnation.org/health
|
||||
@@ -369,14 +363,196 @@ services:
|
||||
failure-threshold: 7
|
||||
success-threshold: 5
|
||||
description: "Healthcheck failed 7 times in a row"
|
||||
- type: mattermost
|
||||
enabled: true
|
||||
- type: messagebird
|
||||
- type: discord
|
||||
enabled: true
|
||||
failure-threshold: 10
|
||||
- type: telegram
|
||||
enabled: true
|
||||
- type: twilio
|
||||
enabled: true
|
||||
failure-threshold: 12
|
||||
success-threshold: 15
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
`))
|
||||
if err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
if config == nil {
|
||||
t.Fatal("Config shouldn't have been nil")
|
||||
}
|
||||
// Alerting providers
|
||||
if config.Alerting == nil {
|
||||
t.Fatal("config.Alerting shouldn't have been nil")
|
||||
}
|
||||
if config.Alerting.Slack == nil || !config.Alerting.Slack.IsValid() {
|
||||
t.Fatal("Slack alerting config should've been valid")
|
||||
}
|
||||
// Services
|
||||
if len(config.Services) != 1 {
|
||||
t.Error("There should've been 1 service")
|
||||
}
|
||||
if config.Services[0].URL != "https://twinnation.org/health" {
|
||||
t.Errorf("URL should have been %s", "https://twinnation.org/health")
|
||||
}
|
||||
if config.Services[0].Interval != 60*time.Second {
|
||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||
}
|
||||
if len(config.Services[0].Alerts) != 7 {
|
||||
t.Fatal("There should've been 7 alerts configured")
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[0].Type != alert.TypeSlack {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Services[0].Alerts[0].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[0].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[0].FailureThreshold != 3 {
|
||||
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Services[0].Alerts[0].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[0].SuccessThreshold != 2 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[0].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[1].Type != alert.TypePagerDuty {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePagerDuty, config.Services[0].Alerts[1].Type)
|
||||
}
|
||||
if config.Services[0].Alerts[1].GetDescription() != "Healthcheck failed 7 times in a row" {
|
||||
t.Errorf("The description of the alert should've been %s, but it was %s", "Healthcheck failed 7 times in a row", config.Services[0].Alerts[1].GetDescription())
|
||||
}
|
||||
if config.Services[0].Alerts[1].FailureThreshold != 7 {
|
||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 7, config.Services[0].Alerts[1].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[1].SuccessThreshold != 5 {
|
||||
t.Errorf("The success threshold of the alert should've been %d, but it was %d", 5, config.Services[0].Alerts[1].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[2].Type != alert.TypeMattermost {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeMattermost, config.Services[0].Alerts[2].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[2].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[2].FailureThreshold != 3 {
|
||||
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Services[0].Alerts[2].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[2].SuccessThreshold != 2 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[2].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[3].Type != alert.TypeMessagebird {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeMessagebird, config.Services[0].Alerts[3].Type)
|
||||
}
|
||||
if config.Services[0].Alerts[3].IsEnabled() {
|
||||
t.Error("The alert should've been disabled")
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[4].Type != alert.TypeDiscord {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeDiscord, config.Services[0].Alerts[4].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[4].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[4].FailureThreshold != 10 {
|
||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Services[0].Alerts[4].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[4].SuccessThreshold != 2 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[4].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[5].Type != alert.TypeTelegram {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTelegram, config.Services[0].Alerts[5].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[5].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[5].FailureThreshold != 3 {
|
||||
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Services[0].Alerts[5].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[5].SuccessThreshold != 2 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[5].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[6].Type != alert.TypeTwilio {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTwilio, config.Services[0].Alerts[6].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[6].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[6].FailureThreshold != 12 {
|
||||
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 12, config.Services[0].Alerts[6].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[6].SuccessThreshold != 15 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.Services[0].Alerts[6].SuccessThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) {
|
||||
config, err := parseAndValidateConfigBytes([]byte(`
|
||||
debug: true
|
||||
|
||||
alerting:
|
||||
slack:
|
||||
webhook-url: "http://example.com"
|
||||
default-alert:
|
||||
enabled: true
|
||||
discord:
|
||||
webhook-url: "http://example.org"
|
||||
default-alert:
|
||||
enabled: true
|
||||
failure-threshold: 10
|
||||
success-threshold: 1
|
||||
pagerduty:
|
||||
integration-key: "00000000000000000000000000000000"
|
||||
default-alert:
|
||||
enabled: true
|
||||
description: default description
|
||||
failure-threshold: 7
|
||||
success-threshold: 5
|
||||
mattermost:
|
||||
webhook-url: "http://example.com"
|
||||
default-alert:
|
||||
enabled: true
|
||||
messagebird:
|
||||
access-key: "1"
|
||||
originator: "31619191918"
|
||||
recipients: "31619191919"
|
||||
default-alert:
|
||||
enabled: false
|
||||
send-on-resolved: true
|
||||
telegram:
|
||||
token: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
|
||||
id: 0123456789
|
||||
default-alert:
|
||||
enabled: true
|
||||
twilio:
|
||||
sid: "1234"
|
||||
token: "5678"
|
||||
from: "+1-234-567-8901"
|
||||
to: "+1-234-567-8901"
|
||||
default-alert:
|
||||
enabled: true
|
||||
failure-threshold: 12
|
||||
success-threshold: 15
|
||||
|
||||
services:
|
||||
- name: twinnation
|
||||
url: https://twinnation.org/health
|
||||
alerts:
|
||||
- type: slack
|
||||
- type: pagerduty
|
||||
- type: mattermost
|
||||
- type: messagebird
|
||||
- type: discord
|
||||
success-threshold: 2 # test service alert override
|
||||
- type: telegram
|
||||
- type: twilio
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
`))
|
||||
if err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
@@ -394,19 +570,36 @@ services:
|
||||
if config.Alerting.Slack == nil || !config.Alerting.Slack.IsValid() {
|
||||
t.Fatal("Slack alerting config should've been valid")
|
||||
}
|
||||
if config.Alerting.Slack.GetDefaultAlert() == nil {
|
||||
t.Fatal("Slack.GetDefaultAlert() shouldn't have returned nil")
|
||||
}
|
||||
if config.Alerting.Slack.WebhookURL != "http://example.com" {
|
||||
t.Errorf("Slack webhook should've been %s, but was %s", "http://example.com", config.Alerting.Slack.WebhookURL)
|
||||
}
|
||||
|
||||
if config.Alerting.PagerDuty == nil || !config.Alerting.PagerDuty.IsValid() {
|
||||
t.Fatal("PagerDuty alerting config should've been valid")
|
||||
}
|
||||
if config.Alerting.PagerDuty.GetDefaultAlert() == nil {
|
||||
t.Fatal("PagerDuty.GetDefaultAlert() shouldn't have returned nil")
|
||||
}
|
||||
if config.Alerting.PagerDuty.IntegrationKey != "00000000000000000000000000000000" {
|
||||
t.Errorf("PagerDuty integration key should've been %s, but was %s", "00000000000000000000000000000000", config.Alerting.PagerDuty.IntegrationKey)
|
||||
}
|
||||
|
||||
if config.Alerting.Mattermost == nil || !config.Alerting.Mattermost.IsValid() {
|
||||
t.Fatal("Mattermost alerting config should've been valid")
|
||||
}
|
||||
if config.Alerting.Mattermost.GetDefaultAlert() == nil {
|
||||
t.Fatal("Mattermost.GetDefaultAlert() shouldn't have returned nil")
|
||||
}
|
||||
|
||||
if config.Alerting.Messagebird == nil || !config.Alerting.Messagebird.IsValid() {
|
||||
t.Fatal("Messagebird alerting config should've been valid")
|
||||
}
|
||||
if config.Alerting.Messagebird.GetDefaultAlert() == nil {
|
||||
t.Fatal("Messagebird.GetDefaultAlert() shouldn't have returned nil")
|
||||
}
|
||||
if config.Alerting.Messagebird.AccessKey != "1" {
|
||||
t.Errorf("Messagebird access key should've been %s, but was %s", "1", config.Alerting.Messagebird.AccessKey)
|
||||
}
|
||||
@@ -420,18 +613,34 @@ services:
|
||||
if config.Alerting.Discord == nil || !config.Alerting.Discord.IsValid() {
|
||||
t.Fatal("Discord alerting config should've been valid")
|
||||
}
|
||||
if config.Alerting.Discord.GetDefaultAlert() == nil {
|
||||
t.Fatal("Discord.GetDefaultAlert() shouldn't have returned nil")
|
||||
}
|
||||
if config.Alerting.Discord.WebhookURL != "http://example.org" {
|
||||
t.Errorf("Discord webhook should've been %s, but was %s", "http://example.org", config.Alerting.Discord.WebhookURL)
|
||||
}
|
||||
if config.Alerting.GetAlertingProviderByAlertType(alert.TypeDiscord) != config.Alerting.Discord {
|
||||
t.Error("expected discord configuration")
|
||||
}
|
||||
|
||||
if config.Alerting.Telegram == nil || !config.Alerting.Telegram.IsValid() {
|
||||
t.Fatal("Telegram alerting config should've been valid")
|
||||
}
|
||||
if config.Alerting.Telegram.GetDefaultAlert() == nil {
|
||||
t.Fatal("Telegram.GetDefaultAlert() shouldn't have returned nil")
|
||||
}
|
||||
if config.Alerting.Telegram.Token != "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" {
|
||||
t.Errorf("Telegram token should've been %s, but was %s", "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", config.Alerting.Telegram.Token)
|
||||
}
|
||||
if config.Alerting.Telegram.ID != "0123456789" {
|
||||
t.Errorf("Telegram ID should've been %s, but was %s", "012345689", config.Alerting.Telegram.ID)
|
||||
}
|
||||
if GetAlertingProviderByAlertType(config, core.DiscordAlert) != config.Alerting.Discord {
|
||||
t.Error("expected discord configuration")
|
||||
|
||||
if config.Alerting.Twilio == nil || !config.Alerting.Twilio.IsValid() {
|
||||
t.Fatal("Twilio alerting config should've been valid")
|
||||
}
|
||||
if config.Alerting.Twilio.GetDefaultAlert() == nil {
|
||||
t.Fatal("Twilio.GetDefaultAlert() shouldn't have returned nil")
|
||||
}
|
||||
// Services
|
||||
if len(config.Services) != 1 {
|
||||
@@ -443,14 +652,14 @@ services:
|
||||
if config.Services[0].Interval != 60*time.Second {
|
||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||
}
|
||||
if len(config.Services[0].Alerts) != 5 {
|
||||
t.Fatal("There should've been 5 alerts configured")
|
||||
if len(config.Services[0].Alerts) != 7 {
|
||||
t.Fatal("There should've been 7 alerts configured")
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[0].Type != core.SlackAlert {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.SlackAlert, config.Services[0].Alerts[0].Type)
|
||||
if config.Services[0].Alerts[0].Type != alert.TypeSlack {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Services[0].Alerts[0].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[0].Enabled {
|
||||
if !config.Services[0].Alerts[0].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[0].FailureThreshold != 3 {
|
||||
@@ -460,31 +669,158 @@ services:
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[0].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[1].Type != core.PagerDutyAlert {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.PagerDutyAlert, config.Services[0].Alerts[1].Type)
|
||||
if config.Services[0].Alerts[1].Type != alert.TypePagerDuty {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePagerDuty, config.Services[0].Alerts[1].Type)
|
||||
}
|
||||
if config.Services[0].Alerts[1].Description != "Healthcheck failed 7 times in a row" {
|
||||
t.Errorf("The description of the alert should've been %s, but it was %s", "Healthcheck failed 7 times in a row", config.Services[0].Alerts[1].Description)
|
||||
if config.Services[0].Alerts[1].GetDescription() != "default description" {
|
||||
t.Errorf("The description of the alert should've been %s, but it was %s", "default description", config.Services[0].Alerts[1].GetDescription())
|
||||
}
|
||||
if config.Services[0].Alerts[1].FailureThreshold != 7 {
|
||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 7, config.Services[0].Alerts[1].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[1].SuccessThreshold != 5 {
|
||||
t.Errorf("The success threshold of the alert should've been %d, but it was %d", 5, config.Services[0].Alerts[1].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[2].Type != core.MessagebirdAlert {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.MessagebirdAlert, config.Services[0].Alerts[2].Type)
|
||||
if config.Services[0].Alerts[2].Type != alert.TypeMattermost {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeMattermost, config.Services[0].Alerts[2].Type)
|
||||
}
|
||||
if config.Services[0].Alerts[2].Enabled {
|
||||
t.Error("The alert should've been disabled")
|
||||
if !config.Services[0].Alerts[2].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[2].FailureThreshold != 3 {
|
||||
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Services[0].Alerts[2].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[2].SuccessThreshold != 2 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[2].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[3].Type != core.DiscordAlert {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.DiscordAlert, config.Services[0].Alerts[3].Type)
|
||||
if config.Services[0].Alerts[3].Type != alert.TypeMessagebird {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeMessagebird, config.Services[0].Alerts[3].Type)
|
||||
}
|
||||
if config.Services[0].Alerts[3].FailureThreshold != 10 {
|
||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Services[0].Alerts[3].FailureThreshold)
|
||||
if config.Services[0].Alerts[3].IsEnabled() {
|
||||
t.Error("The alert should've been disabled")
|
||||
}
|
||||
if config.Services[0].Alerts[3].SuccessThreshold != 2 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[3].SuccessThreshold)
|
||||
if !config.Services[0].Alerts[3].IsSendingOnResolved() {
|
||||
t.Error("The alert should be sending on resolve")
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[4].Type != alert.TypeDiscord {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeDiscord, config.Services[0].Alerts[4].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[4].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[4].FailureThreshold != 10 {
|
||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Services[0].Alerts[4].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[4].SuccessThreshold != 2 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[4].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[5].Type != alert.TypeTelegram {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTelegram, config.Services[0].Alerts[5].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[5].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[5].FailureThreshold != 3 {
|
||||
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Services[0].Alerts[5].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[5].SuccessThreshold != 2 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[5].SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Services[0].Alerts[6].Type != alert.TypeTwilio {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTwilio, config.Services[0].Alerts[6].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[6].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[6].FailureThreshold != 12 {
|
||||
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 12, config.Services[0].Alerts[6].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[6].SuccessThreshold != 15 {
|
||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.Services[0].Alerts[6].SuccessThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) {
|
||||
config, err := parseAndValidateConfigBytes([]byte(`
|
||||
alerting:
|
||||
slack:
|
||||
webhook-url: "http://example.com"
|
||||
default-alert:
|
||||
enabled: true
|
||||
description: "description"
|
||||
|
||||
services:
|
||||
- name: twinnation
|
||||
url: https://twinnation.org/health
|
||||
alerts:
|
||||
- type: slack
|
||||
failure-threshold: 10
|
||||
- type: slack
|
||||
failure-threshold: 20
|
||||
description: "wow"
|
||||
- type: slack
|
||||
enabled: false
|
||||
failure-threshold: 30
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
`))
|
||||
if err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
if config == nil {
|
||||
t.Fatal("Config shouldn't have been nil")
|
||||
}
|
||||
// Alerting providers
|
||||
if config.Alerting == nil {
|
||||
t.Fatal("config.Alerting shouldn't have been nil")
|
||||
}
|
||||
if config.Alerting.Slack == nil || !config.Alerting.Slack.IsValid() {
|
||||
t.Fatal("Slack alerting config should've been valid")
|
||||
}
|
||||
// Services
|
||||
if len(config.Services) != 1 {
|
||||
t.Error("There should've been 2 services")
|
||||
}
|
||||
if config.Services[0].Alerts[0].Type != alert.TypeSlack {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Services[0].Alerts[0].Type)
|
||||
}
|
||||
if config.Services[0].Alerts[1].Type != alert.TypeSlack {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Services[0].Alerts[1].Type)
|
||||
}
|
||||
if config.Services[0].Alerts[2].Type != alert.TypeSlack {
|
||||
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeSlack, config.Services[0].Alerts[2].Type)
|
||||
}
|
||||
if !config.Services[0].Alerts[0].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if !config.Services[0].Alerts[1].IsEnabled() {
|
||||
t.Error("The alert should've been enabled")
|
||||
}
|
||||
if config.Services[0].Alerts[2].IsEnabled() {
|
||||
t.Error("The alert should've been disabled")
|
||||
}
|
||||
if config.Services[0].Alerts[0].GetDescription() != "description" {
|
||||
t.Errorf("The description of the alert should've been %s, but it was %s", "description", config.Services[0].Alerts[0].GetDescription())
|
||||
}
|
||||
if config.Services[0].Alerts[1].GetDescription() != "wow" {
|
||||
t.Errorf("The description of the alert should've been %s, but it was %s", "description", config.Services[0].Alerts[1].GetDescription())
|
||||
}
|
||||
if config.Services[0].Alerts[2].GetDescription() != "description" {
|
||||
t.Errorf("The description of the alert should've been %s, but it was %s", "description", config.Services[0].Alerts[2].GetDescription())
|
||||
}
|
||||
if config.Services[0].Alerts[0].FailureThreshold != 10 {
|
||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Services[0].Alerts[0].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[1].FailureThreshold != 20 {
|
||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 20, config.Services[0].Alerts[1].FailureThreshold)
|
||||
}
|
||||
if config.Services[0].Alerts[2].FailureThreshold != 30 {
|
||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 30, config.Services[0].Alerts[2].FailureThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,8 +1029,7 @@ services:
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithInvalidSecurityConfig(t *testing.T) {
|
||||
defer func() { recover() }()
|
||||
_, _ = parseAndValidateConfigBytes([]byte(`
|
||||
_, err := parseAndValidateConfigBytes([]byte(`
|
||||
security:
|
||||
basic:
|
||||
username: "admin"
|
||||
@@ -705,13 +1040,15 @@ services:
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
`))
|
||||
t.Error("Function should've panicked")
|
||||
if err == nil {
|
||||
t.Error("Function should've returned an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithValidSecurityConfig(t *testing.T) {
|
||||
const expectedUsername = "admin"
|
||||
const expectedPasswordHash = "6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22"
|
||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`debug: true
|
||||
security:
|
||||
basic:
|
||||
username: "%s"
|
||||
@@ -826,8 +1163,7 @@ kubernetes:
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithKubernetesAutoDiscoveryButNoServiceTemplate(t *testing.T) {
|
||||
defer func() { recover() }()
|
||||
_, _ = parseAndValidateConfigBytes([]byte(`
|
||||
_, err := parseAndValidateConfigBytes([]byte(`
|
||||
kubernetes:
|
||||
cluster-mode: "mock"
|
||||
auto-discover: true
|
||||
@@ -836,12 +1172,13 @@ kubernetes:
|
||||
hostname-suffix: ".default.svc.cluster.local"
|
||||
target-path: "/health"
|
||||
`))
|
||||
t.Error("Function should've panicked because providing a service-template is mandatory")
|
||||
if err == nil {
|
||||
t.Error("Function should've returned an error because providing a service-template is mandatory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithKubernetesAutoDiscoveryUsingClusterModeIn(t *testing.T) {
|
||||
defer func() { recover() }()
|
||||
_, _ = parseAndValidateConfigBytes([]byte(`
|
||||
_, err := parseAndValidateConfigBytes([]byte(`
|
||||
kubernetes:
|
||||
cluster-mode: "in"
|
||||
auto-discover: true
|
||||
@@ -854,45 +1191,44 @@ kubernetes:
|
||||
hostname-suffix: ".default.svc.cluster.local"
|
||||
target-path: "/health"
|
||||
`))
|
||||
// TODO: find a way to test this?
|
||||
t.Error("Function should've panicked because testing with ClusterModeIn isn't supported")
|
||||
if err == nil {
|
||||
t.Error("Function should've returned an error because testing with ClusterModeIn isn't supported")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAlertingProviderByAlertType(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Alerting: &alerting.Config{
|
||||
Custom: &custom.AlertProvider{},
|
||||
Discord: &discord.AlertProvider{},
|
||||
Mattermost: &mattermost.AlertProvider{},
|
||||
Messagebird: &messagebird.AlertProvider{},
|
||||
PagerDuty: &pagerduty.AlertProvider{},
|
||||
Slack: &slack.AlertProvider{},
|
||||
Telegram: &telegram.AlertProvider{},
|
||||
Twilio: &twilio.AlertProvider{},
|
||||
},
|
||||
alertingConfig := &alerting.Config{
|
||||
Custom: &custom.AlertProvider{},
|
||||
Discord: &discord.AlertProvider{},
|
||||
Mattermost: &mattermost.AlertProvider{},
|
||||
Messagebird: &messagebird.AlertProvider{},
|
||||
PagerDuty: &pagerduty.AlertProvider{},
|
||||
Slack: &slack.AlertProvider{},
|
||||
Telegram: &telegram.AlertProvider{},
|
||||
Twilio: &twilio.AlertProvider{},
|
||||
}
|
||||
if GetAlertingProviderByAlertType(cfg, core.CustomAlert) != cfg.Alerting.Custom {
|
||||
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeCustom) != alertingConfig.Custom {
|
||||
t.Error("expected Custom configuration")
|
||||
}
|
||||
if GetAlertingProviderByAlertType(cfg, core.DiscordAlert) != cfg.Alerting.Discord {
|
||||
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeDiscord) != alertingConfig.Discord {
|
||||
t.Error("expected Discord configuration")
|
||||
}
|
||||
if GetAlertingProviderByAlertType(cfg, core.MattermostAlert) != cfg.Alerting.Mattermost {
|
||||
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeMattermost) != alertingConfig.Mattermost {
|
||||
t.Error("expected Mattermost configuration")
|
||||
}
|
||||
if GetAlertingProviderByAlertType(cfg, core.MessagebirdAlert) != cfg.Alerting.Messagebird {
|
||||
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeMessagebird) != alertingConfig.Messagebird {
|
||||
t.Error("expected Messagebird configuration")
|
||||
}
|
||||
if GetAlertingProviderByAlertType(cfg, core.PagerDutyAlert) != cfg.Alerting.PagerDuty {
|
||||
if alertingConfig.GetAlertingProviderByAlertType(alert.TypePagerDuty) != alertingConfig.PagerDuty {
|
||||
t.Error("expected PagerDuty configuration")
|
||||
}
|
||||
if GetAlertingProviderByAlertType(cfg, core.SlackAlert) != cfg.Alerting.Slack {
|
||||
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeSlack) != alertingConfig.Slack {
|
||||
t.Error("expected Slack configuration")
|
||||
}
|
||||
if GetAlertingProviderByAlertType(cfg, core.TelegramAlert) != cfg.Alerting.Telegram {
|
||||
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeTelegram) != alertingConfig.Telegram {
|
||||
t.Error("expected Telegram configuration")
|
||||
}
|
||||
if GetAlertingProviderByAlertType(cfg, core.TwilioAlert) != cfg.Alerting.Twilio {
|
||||
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeTwilio) != alertingConfig.Twilio {
|
||||
t.Error("expected Twilio configuration")
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -16,7 +16,7 @@ type WebConfig struct {
|
||||
}
|
||||
|
||||
// validateAndSetDefaults checks and sets the default values for fields that are not set
|
||||
func (web *WebConfig) validateAndSetDefaults() {
|
||||
func (web *WebConfig) validateAndSetDefaults() error {
|
||||
// Validate the Address
|
||||
if len(web.Address) == 0 {
|
||||
web.Address = DefaultAddress
|
||||
@@ -25,8 +25,9 @@ func (web *WebConfig) validateAndSetDefaults() {
|
||||
if web.Port == 0 {
|
||||
web.Port = DefaultPort
|
||||
} else if web.Port < 0 || web.Port > math.MaxUint16 {
|
||||
panic(fmt.Sprintf("invalid port: value should be between %d and %d", 0, math.MaxUint16))
|
||||
return fmt.Errorf("invalid port: value should be between %d and %d", 0, math.MaxUint16)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SocketAddress returns the combination of the Address and the Port
|
||||
|
||||
+11
-12
@@ -44,20 +44,19 @@ func init() {
|
||||
}
|
||||
|
||||
// Handle creates the router and starts the server
|
||||
func Handle() {
|
||||
cfg := config.Get()
|
||||
var router http.Handler = CreateRouter(cfg)
|
||||
func Handle(securityConfig *security.Config, webConfig *config.WebConfig, enableMetrics bool) {
|
||||
var router http.Handler = CreateRouter(securityConfig, enableMetrics)
|
||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||
router = developmentCorsHandler(router)
|
||||
}
|
||||
server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Web.Address, cfg.Web.Port),
|
||||
Addr: fmt.Sprintf("%s:%d", webConfig.Address, webConfig.Port),
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 15 * time.Second,
|
||||
}
|
||||
log.Println("[controller][Handle] Listening on " + cfg.Web.SocketAddress())
|
||||
log.Println("[controller][Handle] Listening on " + webConfig.SocketAddress())
|
||||
if os.Getenv("ROUTER_TEST") == "true" {
|
||||
return
|
||||
}
|
||||
@@ -73,15 +72,15 @@ func Shutdown() {
|
||||
}
|
||||
|
||||
// CreateRouter creates the router for the http server
|
||||
func CreateRouter(cfg *config.Config) *mux.Router {
|
||||
func CreateRouter(securityConfig *security.Config, enabledMetrics bool) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
if cfg.Metrics {
|
||||
if enabledMetrics {
|
||||
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
||||
}
|
||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET")
|
||||
router.HandleFunc("/api/v1/statuses", secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET") // No GzipHandler for this one, because we cache the content
|
||||
router.HandleFunc("/api/v1/statuses/{key}", secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
||||
router.HandleFunc("/api/v1/statuses", secureIfNecessary(securityConfig, serviceStatusesHandler)).Methods("GET") // No GzipHandler for this one, because we cache the content
|
||||
router.HandleFunc("/api/v1/statuses/{key}", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
||||
router.HandleFunc("/api/v1/badges/uptime/{duration}/{identifier}", badgeHandler).Methods("GET")
|
||||
// SPA
|
||||
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
|
||||
@@ -90,9 +89,9 @@ func CreateRouter(cfg *config.Config) *mux.Router {
|
||||
return router
|
||||
}
|
||||
|
||||
func secureIfNecessary(cfg *config.Config, handler http.HandlerFunc) http.HandlerFunc {
|
||||
if cfg.Security != nil && cfg.Security.IsValid() {
|
||||
return security.Handler(handler, cfg.Security)
|
||||
func secureIfNecessary(securityConfig *security.Config, handler http.HandlerFunc) http.HandlerFunc {
|
||||
if securityConfig != nil && securityConfig.IsValid() {
|
||||
return security.Handler(handler, securityConfig)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestCreateRouter(t *testing.T) {
|
||||
}
|
||||
watchdog.UpdateServiceStatuses(cfg.Services[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateServiceStatuses(cfg.Services[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter(cfg)
|
||||
router := CreateRouter(cfg.Security, cfg.Metrics)
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
@@ -235,12 +235,10 @@ func TestHandle(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
defer config.Set(nil)
|
||||
_ = os.Setenv("ROUTER_TEST", "true")
|
||||
_ = os.Setenv("ENVIRONMENT", "dev")
|
||||
defer os.Clearenv()
|
||||
Handle()
|
||||
Handle(cfg.Security, cfg.Web, cfg.Metrics)
|
||||
defer Shutdown()
|
||||
request, _ := http.NewRequest("GET", "/health", nil)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
@@ -273,7 +271,7 @@ func TestServiceStatusesHandler(t *testing.T) {
|
||||
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
|
||||
firstResult.Timestamp = time.Time{}
|
||||
secondResult.Timestamp = time.Time{}
|
||||
router := CreateRouter(&config.Config{})
|
||||
router := CreateRouter(nil, false)
|
||||
|
||||
type Scenario struct {
|
||||
Name string
|
||||
|
||||
+4
-3
@@ -29,16 +29,17 @@ type DNS struct {
|
||||
QueryName string `yaml:"query-name"`
|
||||
}
|
||||
|
||||
func (d *DNS) validateAndSetDefault() {
|
||||
func (d *DNS) validateAndSetDefault() error {
|
||||
if len(d.QueryName) == 0 {
|
||||
panic(ErrDNSWithNoQueryName)
|
||||
return ErrDNSWithNoQueryName
|
||||
}
|
||||
if !strings.HasSuffix(d.QueryName, ".") {
|
||||
d.QueryName += "."
|
||||
}
|
||||
if _, ok := dns.StringToType[d.QueryType]; !ok {
|
||||
panic(ErrDNSWithInvalidQueryType)
|
||||
return ErrDNSWithInvalidQueryType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNS) query(url string, result *Result) {
|
||||
|
||||
+8
-4
@@ -109,8 +109,10 @@ func TestService_ValidateAndSetDefaultsWithNoDNSQueryName(t *testing.T) {
|
||||
QueryType: "A",
|
||||
QueryName: "",
|
||||
}
|
||||
dns.validateAndSetDefault()
|
||||
t.Fatal("Should've panicked because service`s dns didn't have a query name, which is a mandatory field for dns")
|
||||
err := dns.validateAndSetDefault()
|
||||
if err == nil {
|
||||
t.Fatal("Should've returned an error because service`s dns didn't have a query name, which is a mandatory field for dns")
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ValidateAndSetDefaultsWithInvalidDNSQueryType(t *testing.T) {
|
||||
@@ -119,6 +121,8 @@ func TestService_ValidateAndSetDefaultsWithInvalidDNSQueryType(t *testing.T) {
|
||||
QueryType: "B",
|
||||
QueryName: "example.com",
|
||||
}
|
||||
dns.validateAndSetDefault()
|
||||
t.Fatal("Should've panicked because service`s dns query type is invalid, it needs to be a valid query name like A, AAAA, CNAME...")
|
||||
err := dns.validateAndSetDefault()
|
||||
if err == nil {
|
||||
t.Fatal("Should've returned an error because service`s dns query type is invalid, it needs to be a valid query name like A, AAAA, CNAME...")
|
||||
}
|
||||
}
|
||||
+17
-16
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/client"
|
||||
)
|
||||
|
||||
@@ -72,7 +73,7 @@ type Service struct {
|
||||
Conditions []*Condition `yaml:"conditions"`
|
||||
|
||||
// Alerts is the alerting configuration for the service in case of failure
|
||||
Alerts []*Alert `yaml:"alerts"`
|
||||
Alerts []*alert.Alert `yaml:"alerts"`
|
||||
|
||||
// Insecure is whether to skip verifying the server's certificate chain and host name
|
||||
Insecure bool `yaml:"insecure,omitempty"`
|
||||
@@ -85,7 +86,7 @@ type Service struct {
|
||||
}
|
||||
|
||||
// ValidateAndSetDefaults validates the service's configuration and sets the default value of fields that have one
|
||||
func (service *Service) ValidateAndSetDefaults() {
|
||||
func (service *Service) ValidateAndSetDefaults() error {
|
||||
// Set default values
|
||||
if service.Interval == 0 {
|
||||
service.Interval = 1 * time.Minute
|
||||
@@ -105,32 +106,32 @@ func (service *Service) ValidateAndSetDefaults() {
|
||||
if _, contentTypeHeaderExists := service.Headers[ContentTypeHeader]; !contentTypeHeaderExists && service.GraphQL {
|
||||
service.Headers[ContentTypeHeader] = "application/json"
|
||||
}
|
||||
for _, alert := range service.Alerts {
|
||||
if alert.FailureThreshold <= 0 {
|
||||
alert.FailureThreshold = 3
|
||||
for _, serviceAlert := range service.Alerts {
|
||||
if serviceAlert.FailureThreshold <= 0 {
|
||||
serviceAlert.FailureThreshold = 3
|
||||
}
|
||||
if alert.SuccessThreshold <= 0 {
|
||||
alert.SuccessThreshold = 2
|
||||
if serviceAlert.SuccessThreshold <= 0 {
|
||||
serviceAlert.SuccessThreshold = 2
|
||||
}
|
||||
}
|
||||
if len(service.Name) == 0 {
|
||||
panic(ErrServiceWithNoName)
|
||||
return ErrServiceWithNoName
|
||||
}
|
||||
if len(service.URL) == 0 {
|
||||
panic(ErrServiceWithNoURL)
|
||||
return ErrServiceWithNoURL
|
||||
}
|
||||
if len(service.Conditions) == 0 {
|
||||
panic(ErrServiceWithNoCondition)
|
||||
return ErrServiceWithNoCondition
|
||||
}
|
||||
if service.DNS != nil {
|
||||
service.DNS.validateAndSetDefault()
|
||||
return
|
||||
return service.DNS.validateAndSetDefault()
|
||||
}
|
||||
// Make sure that the request can be created
|
||||
_, err := http.NewRequest(service.Method, service.URL, bytes.NewBuffer([]byte(service.Body)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EvaluateHealth sends a request to the service's URL and evaluates the conditions of the service.
|
||||
@@ -155,13 +156,13 @@ func (service *Service) EvaluateHealth() *Result {
|
||||
}
|
||||
|
||||
// GetAlertsTriggered returns a slice of alerts that have been triggered
|
||||
func (service *Service) GetAlertsTriggered() []Alert {
|
||||
var alerts []Alert
|
||||
func (service *Service) GetAlertsTriggered() []alert.Alert {
|
||||
var alerts []alert.Alert
|
||||
if service.NumberOfFailuresInARow == 0 {
|
||||
return alerts
|
||||
}
|
||||
for _, alert := range service.Alerts {
|
||||
if alert.Enabled && alert.FailureThreshold == service.NumberOfFailuresInARow {
|
||||
if alert.IsEnabled() && alert.FailureThreshold == service.NumberOfFailuresInARow {
|
||||
alerts = append(alerts, *alert)
|
||||
continue
|
||||
}
|
||||
|
||||
+22
-10
@@ -5,6 +5,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
)
|
||||
|
||||
func TestService_ValidateAndSetDefaults(t *testing.T) {
|
||||
@@ -13,7 +15,7 @@ func TestService_ValidateAndSetDefaults(t *testing.T) {
|
||||
Name: "twinnation-health",
|
||||
URL: "https://twinnation.org/health",
|
||||
Conditions: []*Condition{&condition},
|
||||
Alerts: []*Alert{{Type: PagerDutyAlert}},
|
||||
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
if service.Method != "GET" {
|
||||
@@ -28,7 +30,7 @@ func TestService_ValidateAndSetDefaults(t *testing.T) {
|
||||
if len(service.Alerts) != 1 {
|
||||
t.Error("Service should've had 1 alert")
|
||||
}
|
||||
if service.Alerts[0].Enabled {
|
||||
if service.Alerts[0].IsEnabled() {
|
||||
t.Error("Service alert should've defaulted to disabled")
|
||||
}
|
||||
if service.Alerts[0].SuccessThreshold != 2 {
|
||||
@@ -47,8 +49,10 @@ func TestService_ValidateAndSetDefaultsWithNoName(t *testing.T) {
|
||||
URL: "http://example.com",
|
||||
Conditions: []*Condition{&condition},
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
t.Fatal("Should've panicked because service didn't have a name, which is a mandatory field")
|
||||
err := service.ValidateAndSetDefaults()
|
||||
if err == nil {
|
||||
t.Fatal("Should've returned an error because service didn't have a name, which is a mandatory field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ValidateAndSetDefaultsWithNoUrl(t *testing.T) {
|
||||
@@ -59,8 +63,10 @@ func TestService_ValidateAndSetDefaultsWithNoUrl(t *testing.T) {
|
||||
URL: "",
|
||||
Conditions: []*Condition{&condition},
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
t.Fatal("Should've panicked because service didn't have an url, which is a mandatory field")
|
||||
err := service.ValidateAndSetDefaults()
|
||||
if err == nil {
|
||||
t.Fatal("Should've returned an error because service didn't have an url, which is a mandatory field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ValidateAndSetDefaultsWithNoConditions(t *testing.T) {
|
||||
@@ -70,8 +76,10 @@ func TestService_ValidateAndSetDefaultsWithNoConditions(t *testing.T) {
|
||||
URL: "http://example.com",
|
||||
Conditions: nil,
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
t.Fatal("Should've panicked because service didn't have at least 1 condition")
|
||||
err := service.ValidateAndSetDefaults()
|
||||
if err == nil {
|
||||
t.Fatal("Should've returned an error because service didn't have at least 1 condition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||
@@ -85,7 +93,10 @@ func TestService_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||
},
|
||||
Conditions: []*Condition{&conditionSuccess},
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
err := service.ValidateAndSetDefaults()
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
if service.DNS.QueryName != "example.com." {
|
||||
t.Error("Service.dns.query-name should be formatted with . suffix")
|
||||
}
|
||||
@@ -93,11 +104,12 @@ func TestService_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||
|
||||
func TestService_GetAlertsTriggered(t *testing.T) {
|
||||
condition := Condition("[STATUS] == 200")
|
||||
enabled := true
|
||||
service := Service{
|
||||
Name: "twinnation-health",
|
||||
URL: "https://twinnation.org/health",
|
||||
Conditions: []*Condition{&condition},
|
||||
Alerts: []*Alert{{Type: PagerDutyAlert, Enabled: true}},
|
||||
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty, Enabled: &enabled}},
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
if service.NumberOfFailuresInARow != 0 {
|
||||
|
||||
@@ -41,7 +41,7 @@ func extractValue(currentKey string, value interface{}) interface{} {
|
||||
tmp := strings.SplitN(currentKey, "[", 3)
|
||||
arrayIndex, err := strconv.Atoi(strings.Replace(tmp[1], "]", "", 1))
|
||||
if err != nil {
|
||||
return value
|
||||
return nil
|
||||
}
|
||||
currentKey := tmp[0]
|
||||
// if currentKey contains only an index (i.e. [0] or 0)
|
||||
|
||||
@@ -20,6 +20,15 @@ func TestEval(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalWithInvalidData(t *testing.T) {
|
||||
path := "simple"
|
||||
data := `invalid data`
|
||||
_, _, err := Eval(path, []byte(data))
|
||||
if err == nil {
|
||||
t.Error("expected an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalWithInvalidPath(t *testing.T) {
|
||||
path := "errors"
|
||||
data := `{}`
|
||||
@@ -75,6 +84,16 @@ func TestEvalWithArrayOfValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalWithArrayOfValuesAndInvalidIndex(t *testing.T) {
|
||||
path := "ids[wat]"
|
||||
data := `{"ids": [1, 2]}`
|
||||
|
||||
_, _, err := Eval(path, []byte(data))
|
||||
if err == nil {
|
||||
t.Error("Expected an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalWithRootArrayOfValues(t *testing.T) {
|
||||
path := "[1]"
|
||||
data := `[1, 2]`
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/TwinProduction/gatus/config"
|
||||
"github.com/TwinProduction/gatus/controller"
|
||||
@@ -13,37 +14,75 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := loadConfiguration()
|
||||
go watchdog.Monitor(cfg)
|
||||
go controller.Handle()
|
||||
cfg, err := loadConfiguration()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
start(cfg)
|
||||
// Wait for termination signal
|
||||
sig := make(chan os.Signal, 1)
|
||||
signalChannel := make(chan os.Signal, 1)
|
||||
done := make(chan bool, 1)
|
||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
||||
signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sig
|
||||
<-signalChannel
|
||||
log.Println("Received termination signal, attempting to gracefully shut down")
|
||||
controller.Shutdown()
|
||||
err := storage.Get().Save()
|
||||
if err != nil {
|
||||
log.Println("Failed to save storage provider:", err.Error())
|
||||
}
|
||||
stop()
|
||||
save()
|
||||
done <- true
|
||||
}()
|
||||
<-done
|
||||
log.Println("Shutting down")
|
||||
}
|
||||
|
||||
func loadConfiguration() *config.Config {
|
||||
var err error
|
||||
func stop() {
|
||||
watchdog.Shutdown()
|
||||
controller.Shutdown()
|
||||
}
|
||||
|
||||
func save() {
|
||||
err := storage.Get().Save()
|
||||
if err != nil {
|
||||
log.Println("Failed to save storage provider:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func start(cfg *config.Config) {
|
||||
go controller.Handle(cfg.Security, cfg.Web, cfg.Metrics)
|
||||
watchdog.Monitor(cfg)
|
||||
go listenToConfigurationFileChanges(cfg)
|
||||
}
|
||||
|
||||
func loadConfiguration() (cfg *config.Config, err error) {
|
||||
customConfigFile := os.Getenv("GATUS_CONFIG_FILE")
|
||||
if len(customConfigFile) > 0 {
|
||||
err = config.Load(customConfigFile)
|
||||
cfg, err = config.Load(customConfigFile)
|
||||
} else {
|
||||
err = config.LoadDefaultConfiguration()
|
||||
cfg, err = config.LoadDefaultConfiguration()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func listenToConfigurationFileChanges(cfg *config.Config) {
|
||||
for {
|
||||
time.Sleep(30 * time.Second)
|
||||
if cfg.HasLoadedConfigurationFileBeenModified() {
|
||||
log.Println("[main][listenToConfigurationFileChanges] Configuration file has been modified")
|
||||
save()
|
||||
updatedConfig, err := loadConfiguration()
|
||||
if err != nil {
|
||||
if cfg.SkipInvalidConfigUpdate {
|
||||
log.Println("[main][listenToConfigurationFileChanges] Failed to load new configuration:", err.Error())
|
||||
log.Println("[main][listenToConfigurationFileChanges] The configuration file was updated, but it is not valid. The old configuration will continue being used.")
|
||||
// Update the last file modification time to avoid trying to process the same invalid configuration again
|
||||
cfg.UpdateLastFileModTime()
|
||||
continue
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
stop()
|
||||
start(updatedConfig)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return config.Get()
|
||||
}
|
||||
+11
-14
@@ -5,7 +5,6 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/TwinProduction/gatus/config"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
@@ -19,18 +18,16 @@ var (
|
||||
// PublishMetricsForService publishes metrics for the given service and its result.
|
||||
// These metrics will be exposed at /metrics if the metrics are enabled
|
||||
func PublishMetricsForService(service *core.Service, result *core.Result) {
|
||||
if config.Get().Metrics {
|
||||
rwLock.Lock()
|
||||
gauge, exists := gauges[fmt.Sprintf("%s_%s", service.Name, service.URL)]
|
||||
if !exists {
|
||||
gauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Subsystem: "gatus",
|
||||
Name: "tasks",
|
||||
ConstLabels: prometheus.Labels{"service": service.Name, "url": service.URL},
|
||||
}, []string{"status", "success"})
|
||||
gauges[fmt.Sprintf("%s_%s", service.Name, service.URL)] = gauge
|
||||
}
|
||||
rwLock.Unlock()
|
||||
gauge.WithLabelValues(strconv.Itoa(result.HTTPStatus), strconv.FormatBool(result.Success)).Inc()
|
||||
rwLock.Lock()
|
||||
gauge, exists := gauges[fmt.Sprintf("%s_%s", service.Name, service.URL)]
|
||||
if !exists {
|
||||
gauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Subsystem: "gatus",
|
||||
Name: "tasks",
|
||||
ConstLabels: prometheus.Labels{"service": service.Name, "url": service.URL},
|
||||
}, []string{"status", "success"})
|
||||
gauges[fmt.Sprintf("%s_%s", service.Name, service.URL)] = gauge
|
||||
}
|
||||
rwLock.Unlock()
|
||||
gauge.WithLabelValues(strconv.Itoa(result.HTTPStatus), strconv.FormatBool(result.Success)).Inc()
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Handler takes care of security for a given handler with the given security configuratioon
|
||||
// Handler takes care of security for a given handler with the given security configuration
|
||||
func Handler(handler http.HandlerFunc, security *Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
usernameEntered, passwordEntered, ok := r.BasicAuth()
|
||||
|
||||
+23
-12
@@ -1,6 +1,7 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +16,9 @@ var (
|
||||
// Because store.Store is an interface, a nil check wouldn't be sufficient, so instead of doing reflection
|
||||
// every single time Get is called, we'll just lazily keep track of its existence through this variable
|
||||
initialized bool
|
||||
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
)
|
||||
|
||||
// Get retrieves the storage provider
|
||||
@@ -33,31 +37,38 @@ func Get() store.Store {
|
||||
func Initialize(cfg *Config) error {
|
||||
initialized = true
|
||||
var err error
|
||||
if cancelFunc != nil {
|
||||
// Stop the active autoSave task
|
||||
cancelFunc()
|
||||
}
|
||||
if cfg == nil || len(cfg.File) == 0 {
|
||||
log.Println("[storage][Initialize] Creating storage provider")
|
||||
provider, err = memory.NewStore("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider, _ = memory.NewStore("")
|
||||
} else {
|
||||
ctx, cancelFunc = context.WithCancel(context.Background())
|
||||
log.Printf("[storage][Initialize] Creating storage provider with file=%s", cfg.File)
|
||||
provider, err = memory.NewStore(cfg.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go autoSave(7 * time.Minute)
|
||||
go autoSave(7*time.Minute, ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// autoSave automatically calls the Save function of the provider at every interval
|
||||
func autoSave(interval time.Duration) {
|
||||
// autoSave automatically calls the SaveFunc function of the provider at every interval
|
||||
func autoSave(interval time.Duration, ctx context.Context) {
|
||||
for {
|
||||
time.Sleep(interval)
|
||||
log.Printf("[storage][autoSave] Saving")
|
||||
err := provider.Save()
|
||||
if err != nil {
|
||||
log.Println("[storage][autoSave] Save failed:", err.Error())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("[storage][autoSave] Stopping active job")
|
||||
return
|
||||
case <-time.After(interval):
|
||||
log.Printf("[storage][autoSave] Saving")
|
||||
err := provider.Save()
|
||||
if err != nil {
|
||||
log.Println("[storage][autoSave] Save failed:", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
file := t.TempDir() + "/test.db"
|
||||
err := Initialize(&Config{File: file})
|
||||
if err != nil {
|
||||
t.Fatal("shouldn't have returned an error")
|
||||
}
|
||||
if cancelFunc == nil {
|
||||
t.Error("cancelFunc shouldn't have been nil")
|
||||
}
|
||||
if ctx == nil {
|
||||
t.Error("ctx shouldn't have been nil")
|
||||
}
|
||||
// Try to initialize it again
|
||||
err = Initialize(&Config{File: file})
|
||||
if err != nil {
|
||||
t.Fatal("shouldn't have returned an error")
|
||||
}
|
||||
cancelFunc()
|
||||
}
|
||||
|
||||
func TestAutoSave(t *testing.T) {
|
||||
file := t.TempDir() + "/test.db"
|
||||
if err := Initialize(&Config{File: file}); err != nil {
|
||||
t.Fatal("shouldn't have returned an error")
|
||||
}
|
||||
go autoSave(3*time.Millisecond, ctx)
|
||||
time.Sleep(15 * time.Millisecond)
|
||||
cancelFunc()
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
// Store is the interface that each stores should implement
|
||||
type Store interface {
|
||||
// GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus
|
||||
// GetAllServiceStatusesWithResultPagination returns the JSON encoding of all monitored core.ServiceStatus
|
||||
// with a subset of core.Result defined by the page and pageSize parameters
|
||||
GetAllServiceStatusesWithResultPagination(page, pageSize int) map[string]*core.ServiceStatus
|
||||
|
||||
|
||||
+38
-38
@@ -4,96 +4,96 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/TwinProduction/gatus/config"
|
||||
"github.com/TwinProduction/gatus/alerting"
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
)
|
||||
|
||||
// HandleAlerting takes care of alerts to resolve and alerts to trigger based on result success or failure
|
||||
func HandleAlerting(service *core.Service, result *core.Result) {
|
||||
cfg := config.Get()
|
||||
if cfg.Alerting == nil {
|
||||
func HandleAlerting(service *core.Service, result *core.Result, alertingConfig *alerting.Config, debug bool) {
|
||||
if alertingConfig == nil {
|
||||
return
|
||||
}
|
||||
if result.Success {
|
||||
handleAlertsToResolve(service, result, cfg)
|
||||
handleAlertsToResolve(service, result, alertingConfig, debug)
|
||||
} else {
|
||||
handleAlertsToTrigger(service, result, cfg)
|
||||
handleAlertsToTrigger(service, result, alertingConfig, debug)
|
||||
}
|
||||
}
|
||||
|
||||
func handleAlertsToTrigger(service *core.Service, result *core.Result, cfg *config.Config) {
|
||||
func handleAlertsToTrigger(service *core.Service, result *core.Result, alertingConfig *alerting.Config, debug bool) {
|
||||
service.NumberOfSuccessesInARow = 0
|
||||
service.NumberOfFailuresInARow++
|
||||
for _, alert := range service.Alerts {
|
||||
// If the alert hasn't been triggered, move to the next one
|
||||
if !alert.Enabled || alert.FailureThreshold > service.NumberOfFailuresInARow {
|
||||
for _, serviceAlert := range service.Alerts {
|
||||
// If the serviceAlert hasn't been triggered, move to the next one
|
||||
if !serviceAlert.IsEnabled() || serviceAlert.FailureThreshold > service.NumberOfFailuresInARow {
|
||||
continue
|
||||
}
|
||||
if alert.Triggered {
|
||||
if cfg.Debug {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Alert for service=%s with description='%s' has already been TRIGGERED, skipping", service.Name, alert.Description)
|
||||
if serviceAlert.Triggered {
|
||||
if debug {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Alert for service=%s with description='%s' has already been TRIGGERED, skipping", service.Name, serviceAlert.GetDescription())
|
||||
}
|
||||
continue
|
||||
}
|
||||
alertProvider := config.GetAlertingProviderByAlertType(cfg, alert.Type)
|
||||
alertProvider := alertingConfig.GetAlertingProviderByAlertType(serviceAlert.Type)
|
||||
if alertProvider != nil && alertProvider.IsValid() {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Sending %s alert because alert for service=%s with description='%s' has been TRIGGERED", alert.Type, service.Name, alert.Description)
|
||||
customAlertProvider := alertProvider.ToCustomAlertProvider(service, alert, result, false)
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Sending %s serviceAlert because serviceAlert for service=%s with description='%s' has been TRIGGERED", serviceAlert.Type, service.Name, serviceAlert.GetDescription())
|
||||
customAlertProvider := alertProvider.ToCustomAlertProvider(service, serviceAlert, result, false)
|
||||
// TODO: retry on error
|
||||
var err error
|
||||
// We need to extract the DedupKey from PagerDuty's response
|
||||
if alert.Type == core.PagerDutyAlert {
|
||||
if serviceAlert.Type == alert.TypePagerDuty {
|
||||
var body []byte
|
||||
if body, err = customAlertProvider.Send(service.Name, alert.Description, false); err == nil {
|
||||
if body, err = customAlertProvider.Send(service.Name, serviceAlert.GetDescription(), false); err == nil {
|
||||
var response pagerDutyResponse
|
||||
if err = json.Unmarshal(body, &response); err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error unmarshaling pagerduty response: %s", err.Error())
|
||||
} else {
|
||||
alert.ResolveKey = response.DedupKey
|
||||
serviceAlert.ResolveKey = response.DedupKey
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// All other alert types don't need to extract anything from the body, so we can just send the request right away
|
||||
_, err = customAlertProvider.Send(service.Name, alert.Description, false)
|
||||
// All other serviceAlert types don't need to extract anything from the body, so we can just send the request right away
|
||||
_, err = customAlertProvider.Send(service.Name, serviceAlert.GetDescription(), false)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Failed to send an alert for service=%s: %s", service.Name, err.Error())
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Failed to send an serviceAlert for service=%s: %s", service.Name, err.Error())
|
||||
} else {
|
||||
alert.Triggered = true
|
||||
serviceAlert.Triggered = true
|
||||
}
|
||||
} else {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Not sending alert of type=%s despite being TRIGGERED, because the provider wasn't configured properly", alert.Type)
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Not sending serviceAlert of type=%s despite being TRIGGERED, because the provider wasn't configured properly", serviceAlert.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAlertsToResolve(service *core.Service, result *core.Result, cfg *config.Config) {
|
||||
func handleAlertsToResolve(service *core.Service, result *core.Result, alertingConfig *alerting.Config, debug bool) {
|
||||
service.NumberOfSuccessesInARow++
|
||||
for _, alert := range service.Alerts {
|
||||
if !alert.Enabled || !alert.Triggered || alert.SuccessThreshold > service.NumberOfSuccessesInARow {
|
||||
for _, serviceAlert := range service.Alerts {
|
||||
if !serviceAlert.IsEnabled() || !serviceAlert.Triggered || serviceAlert.SuccessThreshold > service.NumberOfSuccessesInARow {
|
||||
continue
|
||||
}
|
||||
// Even if the alert provider returns an error, we still set the alert's Triggered variable to false.
|
||||
// Even if the serviceAlert provider returns an error, we still set the serviceAlert's Triggered variable to false.
|
||||
// Further explanation can be found on Alert's Triggered field.
|
||||
alert.Triggered = false
|
||||
if !alert.SendOnResolved {
|
||||
serviceAlert.Triggered = false
|
||||
if !serviceAlert.IsSendingOnResolved() {
|
||||
continue
|
||||
}
|
||||
alertProvider := config.GetAlertingProviderByAlertType(cfg, alert.Type)
|
||||
alertProvider := alertingConfig.GetAlertingProviderByAlertType(serviceAlert.Type)
|
||||
if alertProvider != nil && alertProvider.IsValid() {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Sending %s alert because alert for service=%s with description='%s' has been RESOLVED", alert.Type, service.Name, alert.Description)
|
||||
customAlertProvider := alertProvider.ToCustomAlertProvider(service, alert, result, true)
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Sending %s serviceAlert because serviceAlert for service=%s with description='%s' has been RESOLVED", serviceAlert.Type, service.Name, serviceAlert.GetDescription())
|
||||
customAlertProvider := alertProvider.ToCustomAlertProvider(service, serviceAlert, result, true)
|
||||
// TODO: retry on error
|
||||
_, err := customAlertProvider.Send(service.Name, alert.Description, true)
|
||||
_, err := customAlertProvider.Send(service.Name, serviceAlert.GetDescription(), true)
|
||||
if err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Failed to send an alert for service=%s: %s", service.Name, err.Error())
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Failed to send an serviceAlert for service=%s: %s", service.Name, err.Error())
|
||||
} else {
|
||||
if alert.Type == core.PagerDutyAlert {
|
||||
alert.ResolveKey = ""
|
||||
if serviceAlert.Type == alert.TypePagerDuty {
|
||||
serviceAlert.ResolveKey = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Not sending alert of type=%s despite being RESOLVED, because the provider wasn't configured properly", alert.Type)
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Not sending serviceAlert of type=%s despite being RESOLVED, because the provider wasn't configured properly", serviceAlert.Type)
|
||||
}
|
||||
}
|
||||
service.NumberOfFailuresInARow = 0
|
||||
|
||||
+71
-78
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting"
|
||||
"github.com/TwinProduction/gatus/alerting/alert"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||
"github.com/TwinProduction/gatus/alerting/provider/pagerduty"
|
||||
"github.com/TwinProduction/gatus/config"
|
||||
@@ -24,78 +25,69 @@ func TestHandleAlerting(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
enabled := true
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
Alerts: []*alert.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
Type: alert.TypeCustom,
|
||||
Enabled: &enabled,
|
||||
FailureThreshold: 2,
|
||||
SuccessThreshold: 3,
|
||||
SendOnResolved: true,
|
||||
SendOnResolved: &enabled,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
verify(t, service, 0, 0, false, "The alert shouldn't start triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 1, 0, false, "The alert shouldn't have triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 2, 0, true, "The alert should've triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 3, 0, true, "The alert should still be triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 4, 0, true, "The alert should still be triggered")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 1, true, "The alert should still be triggered (because service.Alerts[0].SuccessThreshold is 3)")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 2, true, "The alert should still be triggered (because service.Alerts[0].SuccessThreshold is 3)")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 3, false, "The alert should've been resolved")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 4, false, "The alert should no longer be triggered")
|
||||
}
|
||||
|
||||
func TestHandleAlertingWhenAlertingConfigIsNil(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Debug: true,
|
||||
Alerting: nil,
|
||||
}
|
||||
config.Set(cfg)
|
||||
HandleAlerting(nil, nil)
|
||||
HandleAlerting(nil, nil, nil, true)
|
||||
}
|
||||
|
||||
func TestHandleAlertingWithBadAlertProvider(t *testing.T) {
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||
defer os.Clearenv()
|
||||
|
||||
cfg := &config.Config{
|
||||
Alerting: &alerting.Config{},
|
||||
}
|
||||
config.Set(cfg)
|
||||
enabled := true
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
Alerts: []*alert.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
Type: alert.TypeCustom,
|
||||
Enabled: &enabled,
|
||||
FailureThreshold: 1,
|
||||
SuccessThreshold: 1,
|
||||
SendOnResolved: true,
|
||||
SendOnResolved: &enabled,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
verify(t, service, 0, 0, false, "The alert shouldn't start triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, &alerting.Config{}, false)
|
||||
verify(t, service, 1, 0, false, "The alert shouldn't have triggered")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, &alerting.Config{}, false)
|
||||
verify(t, service, 2, 0, false, "The alert shouldn't have triggered, because the provider wasn't configured properly")
|
||||
}
|
||||
|
||||
@@ -112,16 +104,16 @@ func TestHandleAlertingWhenTriggeredAlertIsAlmostResolvedButServiceStartFailingA
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
enabled := true
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
Alerts: []*alert.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
Type: alert.TypeCustom,
|
||||
Enabled: &enabled,
|
||||
FailureThreshold: 2,
|
||||
SuccessThreshold: 3,
|
||||
SendOnResolved: true,
|
||||
SendOnResolved: &enabled,
|
||||
Triggered: true,
|
||||
},
|
||||
},
|
||||
@@ -129,7 +121,7 @@ func TestHandleAlertingWhenTriggeredAlertIsAlmostResolvedButServiceStartFailingA
|
||||
}
|
||||
|
||||
// This test simulate an alert that was already triggered
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 2, 0, true, "The alert was already triggered at the beginning of this test")
|
||||
}
|
||||
|
||||
@@ -146,23 +138,24 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedButSendOnResolvedIsFalse(t *t
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
enabled := true
|
||||
disabled := false
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
Alerts: []*alert.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
Type: alert.TypeCustom,
|
||||
Enabled: &enabled,
|
||||
FailureThreshold: 1,
|
||||
SuccessThreshold: 1,
|
||||
SendOnResolved: false,
|
||||
SendOnResolved: &disabled,
|
||||
Triggered: true,
|
||||
},
|
||||
},
|
||||
NumberOfFailuresInARow: 1,
|
||||
}
|
||||
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 1, false, "The alert should've been resolved")
|
||||
}
|
||||
|
||||
@@ -178,26 +171,26 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedPagerDuty(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
enabled := true
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
Alerts: []*alert.Alert{
|
||||
{
|
||||
Type: core.PagerDutyAlert,
|
||||
Enabled: true,
|
||||
Type: alert.TypePagerDuty,
|
||||
Enabled: &enabled,
|
||||
FailureThreshold: 1,
|
||||
SuccessThreshold: 1,
|
||||
SendOnResolved: true,
|
||||
SendOnResolved: &enabled,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
NumberOfFailuresInARow: 0,
|
||||
}
|
||||
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 1, 0, true, "")
|
||||
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 1, false, "The alert should've been resolved")
|
||||
}
|
||||
|
||||
@@ -214,48 +207,48 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
enabled := true
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
Alerts: []*alert.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
Type: alert.TypeCustom,
|
||||
Enabled: &enabled,
|
||||
FailureThreshold: 2,
|
||||
SuccessThreshold: 2,
|
||||
SendOnResolved: true,
|
||||
SendOnResolved: &enabled,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 1, 0, false, "")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 2, 0, false, "The alert should have failed to trigger, because the alert provider is returning an error")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 3, 0, false, "The alert should still not be triggered, because the alert provider is still returning an error")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 4, 0, false, "The alert should still not be triggered, because the alert provider is still returning an error")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 5, 0, true, "The alert should've been triggered because the alert provider is no longer returning an error")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 1, true, "The alert should've still been triggered")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 2, false, "The alert should've been resolved DESPITE THE ALERT PROVIDER RETURNING AN ERROR. See Alert.Triggered for further explanation.")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||
|
||||
// Make sure that everything's working as expected after a rough patch
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 1, 0, false, "")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 2, 0, true, "The alert should have triggered")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 1, true, "The alert should still be triggered")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 2, false, "The alert should have been resolved")
|
||||
}
|
||||
|
||||
@@ -272,42 +265,42 @@ func TestHandleAlertingWithProviderThatOnlyReturnsErrorOnResolve(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Set(cfg)
|
||||
enabled := true
|
||||
service := &core.Service{
|
||||
URL: "http://example.com",
|
||||
Alerts: []*core.Alert{
|
||||
Alerts: []*alert.Alert{
|
||||
{
|
||||
Type: core.CustomAlert,
|
||||
Enabled: true,
|
||||
Type: alert.TypeCustom,
|
||||
Enabled: &enabled,
|
||||
FailureThreshold: 1,
|
||||
SuccessThreshold: 1,
|
||||
SendOnResolved: true,
|
||||
SendOnResolved: &enabled,
|
||||
Triggered: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 1, 0, true, "")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 1, false, "")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 1, 0, true, "")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 1, false, "")
|
||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||
|
||||
// Make sure that everything's working as expected after a rough patch
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 1, 0, true, "")
|
||||
HandleAlerting(service, &core.Result{Success: false})
|
||||
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 2, 0, true, "")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 1, false, "")
|
||||
HandleAlerting(service, &core.Result{Success: true})
|
||||
HandleAlerting(service, &core.Result{Success: true}, cfg.Alerting, cfg.Debug)
|
||||
verify(t, service, 0, 2, false, "")
|
||||
}
|
||||
|
||||
|
||||
+52
-28
@@ -1,10 +1,12 @@
|
||||
package watchdog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/TwinProduction/gatus/alerting"
|
||||
"github.com/TwinProduction/gatus/config"
|
||||
"github.com/TwinProduction/gatus/core"
|
||||
"github.com/TwinProduction/gatus/metric"
|
||||
@@ -15,48 +17,65 @@ var (
|
||||
// monitoringMutex is used to prevent multiple services from being evaluated at the same time.
|
||||
// Without this, conditions using response time may become inaccurate.
|
||||
monitoringMutex sync.Mutex
|
||||
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
)
|
||||
|
||||
// Monitor loops over each services and starts a goroutine to monitor each services separately
|
||||
func Monitor(cfg *config.Config) {
|
||||
ctx, cancelFunc = context.WithCancel(context.Background())
|
||||
for _, service := range cfg.Services {
|
||||
// To prevent multiple requests from running at the same time, we'll wait for a little bit before each iteration
|
||||
time.Sleep(1111 * time.Millisecond)
|
||||
go monitor(service)
|
||||
go monitor(service, cfg.Alerting, cfg.DisableMonitoringLock, cfg.Metrics, cfg.Debug, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// monitor monitors a single service in a loop
|
||||
func monitor(service *core.Service) {
|
||||
cfg := config.Get()
|
||||
func monitor(service *core.Service, alertingConfig *alerting.Config, disableMonitoringLock, enabledMetrics, debug bool, ctx context.Context) {
|
||||
// Run it immediately on start
|
||||
execute(service, alertingConfig, disableMonitoringLock, enabledMetrics, debug)
|
||||
// Loop for the next executions
|
||||
for {
|
||||
if !cfg.DisableMonitoringLock {
|
||||
// By placing the lock here, we prevent multiple services from being monitored at the exact same time, which
|
||||
// could cause performance issues and return inaccurate results
|
||||
monitoringMutex.Lock()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("[watchdog][monitor] Canceling current execution of group=%s; service=%s", service.Group, service.Name)
|
||||
return
|
||||
case <-time.After(service.Interval):
|
||||
execute(service, alertingConfig, disableMonitoringLock, enabledMetrics, debug)
|
||||
}
|
||||
if cfg.Debug {
|
||||
log.Printf("[watchdog][monitor] Monitoring group=%s; service=%s", service.Group, service.Name)
|
||||
}
|
||||
result := service.EvaluateHealth()
|
||||
}
|
||||
}
|
||||
|
||||
func execute(service *core.Service, alertingConfig *alerting.Config, disableMonitoringLock, enabledMetrics, debug bool) {
|
||||
if !disableMonitoringLock {
|
||||
// By placing the lock here, we prevent multiple services from being monitored at the exact same time, which
|
||||
// could cause performance issues and return inaccurate results
|
||||
monitoringMutex.Lock()
|
||||
}
|
||||
if debug {
|
||||
log.Printf("[watchdog][execute] Monitoring group=%s; service=%s", service.Group, service.Name)
|
||||
}
|
||||
result := service.EvaluateHealth()
|
||||
if enabledMetrics {
|
||||
metric.PublishMetricsForService(service, result)
|
||||
UpdateServiceStatuses(service, result)
|
||||
log.Printf(
|
||||
"[watchdog][monitor] Monitored group=%s; service=%s; success=%v; errors=%d; duration=%s",
|
||||
service.Group,
|
||||
service.Name,
|
||||
result.Success,
|
||||
len(result.Errors),
|
||||
result.Duration.Round(time.Millisecond),
|
||||
)
|
||||
HandleAlerting(service, result)
|
||||
if cfg.Debug {
|
||||
log.Printf("[watchdog][monitor] Waiting for interval=%s before monitoring group=%s service=%s again", service.Interval, service.Group, service.Name)
|
||||
}
|
||||
if !cfg.DisableMonitoringLock {
|
||||
monitoringMutex.Unlock()
|
||||
}
|
||||
time.Sleep(service.Interval)
|
||||
}
|
||||
UpdateServiceStatuses(service, result)
|
||||
log.Printf(
|
||||
"[watchdog][execute] Monitored group=%s; service=%s; success=%v; errors=%d; duration=%s",
|
||||
service.Group,
|
||||
service.Name,
|
||||
result.Success,
|
||||
len(result.Errors),
|
||||
result.Duration.Round(time.Millisecond),
|
||||
)
|
||||
HandleAlerting(service, result, alertingConfig, debug)
|
||||
if debug {
|
||||
log.Printf("[watchdog][execute] Waiting for interval=%s before monitoring group=%s service=%s again", service.Interval, service.Group, service.Name)
|
||||
}
|
||||
if !disableMonitoringLock {
|
||||
monitoringMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,3 +83,8 @@ func monitor(service *core.Service) {
|
||||
func UpdateServiceStatuses(service *core.Service, result *core.Result) {
|
||||
storage.Get().Insert(service, result)
|
||||
}
|
||||
|
||||
// Shutdown stops monitoring all services
|
||||
func Shutdown() {
|
||||
cancelFunc()
|
||||
}
|
||||
Generated
+7603
-11943
File diff suppressed because it is too large.
Load diff
+16
-17
@@ -1,30 +1,29 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "0.1.0",
|
||||
"name": "gatus",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --mode development",
|
||||
"build": "vue-cli-service build --mode production",
|
||||
"build": "vue-cli-service build --modern --mode production",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0-0"
|
||||
"core-js": "^3.12.1",
|
||||
"vue": "^3.0.11",
|
||||
"vue-router": "^4.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss7-compat": "^2.1.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"@vue/cli-plugin-babel": "^5.0.0-beta.0",
|
||||
"@vue/cli-plugin-eslint": "^5.0.0-beta.0",
|
||||
"@vue/cli-plugin-router": "^5.0.0-beta.0",
|
||||
"@vue/cli-service": "^5.0.0-beta.0",
|
||||
"@vue/compiler-sfc": "^3.0.11",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"postcss": "^7.0.35",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.1.0"
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-plugin-vue": "^7.8.0",
|
||||
"postcss": "^8.2.15",
|
||||
"tailwindcss": "^2.1.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta charset="utf-8" />
|
||||
<title>Health Dashboard | Gatus</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Health Dashboard | Gatus</title>
|
||||
</head>
|
||||
<body class="dark:bg-gray-900">
|
||||
<noscript>
|
||||
<strong>Enable JavaScript to view this page.</strong>
|
||||
</noscript>
|
||||
<noscript><strong>Enable JavaScript to view this page.</strong></noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -52,7 +52,6 @@ export default {
|
||||
this.applyTheme();
|
||||
},
|
||||
applyTheme() {
|
||||
console.log(localStorage.theme, 'theme' in localStorage);
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
this.darkMode = true;
|
||||
document.documentElement.classList.add('dark');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#social[data-v-1cbbc992]{position:fixed;right:5px;bottom:5px;padding:5px;margin:0;z-index:100}#social img[data-v-1cbbc992]{opacity:.3}#social img[data-v-1cbbc992]:hover{opacity:1}#tooltip{position:fixed;background-color:#fff;border:1px solid #d3d3d3;border-radius:4px;padding:6px;font-size:13px}#tooltip code{color:#212529;line-height:1}#tooltip .tooltip-title{font-weight:700;margin-bottom:0;display:block;margin-top:8px}#tooltip>.tooltip-title:first-child{margin-top:0}
|
||||
|
||||
/*! tailwindcss v2.1.0 | MIT License | https://tailwindcss.com */
|
||||
/*! tailwindcss v2.1.2 | MIT License | https://tailwindcss.com */
|
||||
|
||||
/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */:root{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Consolas,Monaco,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgba(254,226,226,var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220,38,38,var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgba(209,250,229,var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.dark .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}.dark .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgba(17,24,39,var(--tw-bg-opacity))}.dark .dark\:hover\:bg-gray-600:hover{--tw-bg-opacity:1;background-color:rgba(75,85,99,var(--tw-bg-opacity))}.dark .dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgba(156,163,175,var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239,68,68,var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgba(5,150,105,var(--tw-border-opacity))}.dark .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.rounded-none{border-radius:0}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.border-dashed{border-style:dashed}.border{border-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.cursor-pointer{cursor:pointer}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:tablLine truncated
|
||||
/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */html{-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Consolas,Monaco,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgba(254,226,226,var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220,38,38,var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgba(209,250,229,var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.dark .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}.dark .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgba(17,24,39,var(--tw-bg-opacity))}.dark .dark\:hover\:bg-gray-600:hover{--tw-bg-opacity:1;background-color:rgba(75,85,99,var(--tw-bg-opacity))}.dark .dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgba(156,163,175,var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239,68,68,var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgba(5,150,105,var(--tw-border-opacity))}.dark .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.rounded-none{border-radius:0}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.border-dashed{border-style:dashed}.border{border-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.cursor-pointer{cursor:pointer}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:table}.hidLine truncated
|
||||
@@ -1 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>Health Dashboard | Gatus</title><link href="/css/app.css" rel="preload" as="style"><link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"><link href="/css/app.css" rel="stylesheet"></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.js"></script><script src="/js/app.js"></script></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>Health Dashboard | Gatus</title><script defer="defer" src="/js/chunk-vendors.js" type="module"></script><script defer="defer" src="/js/app.js" type="module"></script><link href="/css/app.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.js" nomodule></script><script defer="defer" src="/js/app-legacy.js" nomodule></script></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
(function(){"use strict";var e={8187:function(e,t,r){r.d(t,{L:function(){return ut}});r(6992),r(8674),r(9601),r(7727);var s=r(9963),n=r(6252),o=r.p+"img/logo.png",a={class:"container container-xs relative mx-auto xl:rounded xl:border xl:shadow-xl xl:my-5 p-5 pb-12 xl:pb-5 text-left dark:bg-gray-800 dark:text-gray-200 dark:border-gray-500",id:"global"},i=(0,n.Wm)("div",{class:"mb-2"},[(0,n.Wm)("div",{class:"flex flex-wrap"},[(0,n.Wm)("div",{class:"w-3/4 text-left my-auto"},[(0,n.Wm)("div",{class:"text-3xl xl:text-5xl lg:text-4xl font-light"},"Health Status")]),(0,n.Wm)("div",{class:"w-1/4 flex justify-end"},[(0,n.Wm)("img",{src:o,alt:"Gatus",class:"object-scale-down",style:{"max-width":"100px","min-width":"50px","min-height":"50px"}})])])],-1);function l(e,t,r,s,o,l){var u=(0,n.up)("router-view"),g=(0,n.up)("Tooltip"),c=(0,n.up)("Social");return(0,n.wg)(),(0,n.j4)(n.HY,null,[(0,n.Wm)("div",a,[i,(0,n.Wm)(u,{onShowTooltip:l.showTooltip},null,8,["onShowTooltip"])]),(0,n.Wm)(g,{result:o.tooltip.result,event:o.tooltip.event},null,8,["result","event"]),(0,n.Wm)(c)],64)}var u=r.p+"img/github.png",g=(0,n.HX)("data-v-1cbbc992");(0,n.dD)("data-v-1cbbc992");var c={id:"social"},h=(0,n.Wm)("a",{href:"https://github.com/TwinProduction/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,n.Wm)("img",{src:u,alt:"GitHub",width:"32",height:"auto"})],-1);(0,n.Cn)();var d=g((function(e,t,r,s,o,a){return(0,n.wg)(),(0,n.j4)("div",c,[h])})),m={name:"Social"};m.render=d,m.__scopeId="data-v-1cbbc992";var f=m,v=(r(6977),r(3577)),p=(0,n.Wm)("div",{class:"tooltip-title"},"Timestamp:",-1),A={id:"tooltip-timestamp"},w=(0,n.Wm)("div",{class:"tooltip-title"},"Response time:",-1),y={id:"tooltip-response-time"},x=(0,n.Wm)("div",{class:"tooltip-title"},"Conditions:",-1),T={id:"tooltip-conditions"},I=(0,n.Wm)("br",null,null,-1),b={key:0,id:"tooltip-errors-container"},k=(0,n.Wm)("div",{class:"tooltip-title"},"Errors:",-1),R={id:"tooltip-errors"},S=(0,n.Wm)("br",null,null,-1);function W(e,t,r,s,o,a){return(0,n.wg)(),(0,n.j4)("div",{id:"tooltip",ref:"tooltip",class:o.hidden?"invisible":"",style:"top:"+o.top+"px; left:"+o.left+"px"},[r.result?(0,n.WI)(e.$slots,"default",{key:0},(function(){return[p,(0,n.Wm)("code",A,(0,v.zw)(a.prettifyTimestamp(r.result.timestamp)),1),w,(0,n.Wm)("code",y,(0,v.zw)((r.result.duration/1e6).toFixed(0))+"ms",1),x,(0,n.Wm)("code",T,[((0,n.wg)(!0),(0,n.j4)(n.HY,null,(0,n.Ko)(r.result.conditionResults,(function(t){return(0,n.WI)(e.$slots,"default",{key:t},(function(){return[(0,n.Uk)((0,v.zw)(t.success?"✓":"X")+" ~ "+(0,v.zw)(t.condition),1),I]}))})),128))]),r.result.errors&&r.result.errors.length?((0,n.wg)(),(0,n.j4)("div",b,[k,(0,n.Wm)("code",R,[((0,n.wg)(!0),(0,n.j4)(n.HY,null,(0,n.Ko)(r.result.errors,(function(t){return(0,n.WI)(e.$slots,"default",{key:t},(function(){return[(0,n.Uk)(" - "+(0,v.zw)(t),1),S]}))})),128))])])):(0,n.ry)("",!0)]})):(0,n.ry)("",!0)],6)}r(4916),r(5306);var C={name:"Services",props:{event:Event,result:Object},methods:{prettifyTimestamp:function(e){var t=new Date(e),r=t.getFullYear(),s=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),n=(t.getDate()<10?"0":"")+t.getDate(),o=(t.getHours()<10?"0":"")+t.getHours(),a=(t.getMinutes()<10?"0":"")+t.getMinutes(),i=(t.getSeconds()<10?"0":"")+t.getSeconds();return r+"-"+s+"-"+n+" "+o+":"+a+":"+i},htmlEntities:function(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")},reposition:function(){if(this.event&&this.event.type)if("mouseenter"===this.event.type){var e=this.event.target.getBoundingClientRect().y+30,t=this.event.target.getBoundingClientRect().x,r=this.$refs.tooltip.getBoundingClientRect();t+window.scrollX+r.width+50>document.body.getBoundingClientRect().width&&(t=this.event.target.getBoundingClientRect().x-r.width+this.event.target.getBoundingClientRect().width,t<0&&(t+=-t)),e+window.scrollY+r.height+50>document.body.getBoundingClientRect().height&&e>=0&&(e=this.event.target.getBoundingClientRect().y-(r.height+10),e<0&&(e=this.event.target.getBoundingClientRect().y+30)),this.top=e,this.left=t}else"mouseleave"===this.event.type&&(this.hidden=!0)}},watch:{event:function(e){e&&e.type&&("mouseenter"===e.type?this.hidden=!1:"mouseleave"===e.type&&(this.hidden=!0))}},updated:function(){this.reposition()},created:function(){this.reposition()},data:function(){return{hidden:!0,top:0,left:0}}};C.render=W;var B=C,P={name:"App",components:{Social:f,Tooltip:B},methods:{showTooltip:function(e,t){this.tooltip={result:e,event:t}}},data:function(){return{tooltip:{}}}};P.render=l;var D=P,E=r(2119);function H(e,t,r,s,o,a){var i=(0,n.up)("Services"),l=(0,n.up)("Pagination"),u=(0,n.up)("Settings");return(0,n.wg)(),(0,n.j4)(n.HY,null,[(0,n.Wm)(i,{serviceStatuses:o.serviceStatuses,showStatusOnHover:!0,onShowTooltip:a.showTooltip,onToggleShowAverageResponseTime:a.toggleShowAverageResponseTime,showAverageResponseTime:o.showAverageResponseTime},null,8,["serviceStatuses","onShowTooltip","onToggleShowALine truncated
|
||||
@@ -1 +1 @@
|
||||
(function(e){function t(t){for(var s,a,i=t[0],c=t[1],l=t[2],u=0,d=[];u<i.length;u++)a=i[u],Object.prototype.hasOwnProperty.call(r,a)&&r[a]&&d.push(r[a][0]),r[a]=0;for(s in c)Object.prototype.hasOwnProperty.call(c,s)&&(e[s]=c[s]);g&&g(t);while(d.length)d.shift()();return o.push.apply(o,l||[]),n()}function n(){for(var e,t=0;t<o.length;t++){for(var n=o[t],s=!0,i=1;i<n.length;i++){var c=n[i];0!==r[c]&&(s=!1)}s&&(o.splice(t--,1),e=a(a.s=n[0]))}return e}var s={},r={app:0},o=[];function a(t){if(s[t])return s[t].exports;var n=s[t]={i:t,l:!1,exports:{}};return e[t].call(n.exports,n,n.exports,a),n.l=!0,n.exports}a.m=e,a.c=s,a.d=function(e,t,n){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},a.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,t){if(1&t&&(e=a(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(a.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)a.d(n,s,function(t){return e[t]}.bind(null,s));return n},a.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="/";var i=window["webpackJsonp"]=window["webpackJsonp"]||[],c=i.push.bind(i);i.push=t,i=i.slice();for(var l=0;l<i.length;l++)t(i[l]);var g=c;o.push([0,"chunk-vendors"]),n()})({0:function(e,t,n){e.exports=n("56d7")},1289:function(e,t,n){"use strict";n("d502")},"17e7":function(e,t,n){"use strict";n("31c1")},"1dd9":function(e,t,n){"use strict";n("ae5b")},"22c8":function(e,t,n){},"31c1":function(e,t,n){},"359c":function(e,t,n){e.exports=n.p+"img/github.png"},"508b":function(e,t,n){"use strict";n("22c8")},5661:function(e,t,n){"use strict";n("bf65")},"56d7":function(e,t,n){"use strict";n.r(t),n.d(t,"SERVER_URL",(function(){return dt}));n("e260"),n("e6cf"),n("cca6"),n("a79d");var s=n("7a23"),r=n("cf05"),o=n.n(r),a={class:"container container-xs relative mx-auto xl:rounded xl:border xl:shadow-xl xl:my-5 p-5 pb-12 xl:pb-5 text-left dark:bg-gray-800 dark:text-gray-200 dark:border-gray-500",id:"global"},i=Object(s["g"])("div",{class:"mb-2"},[Object(s["g"])("div",{class:"flex flex-wrap"},[Object(s["g"])("div",{class:"w-3/4 text-left my-auto"},[Object(s["g"])("div",{class:"text-3xl xl:text-5xl lg:text-4xl font-light"},"Health Status")]),Object(s["g"])("div",{class:"w-1/4 flex justify-end"},[Object(s["g"])("img",{src:o.a,alt:"Gatus",class:"object-scale-down",style:{"max-width":"100px","min-width":"50px","min-height":"50px"}})])])],-1);function c(e,t,n,r,o,c){var l=Object(s["x"])("router-view"),g=Object(s["x"])("Tooltip"),u=Object(s["x"])("Social");return Object(s["p"])(),Object(s["d"])(s["a"],null,[Object(s["g"])("div",a,[i,Object(s["g"])(l,{onShowTooltip:c.showTooltip},null,8,["onShowTooltip"])]),Object(s["g"])(g,{result:o.tooltip.result,event:o.tooltip.event},null,8,["result","event"]),Object(s["g"])(u)],64)}var l=n("359c"),g=n.n(l),u=Object(s["D"])("data-v-1cbbc992");Object(s["s"])("data-v-1cbbc992");var d={id:"social"},h=Object(s["g"])("a",{href:"https://github.com/TwinProduction/gatus",target:"_blank",title:"Gatus on GitHub"},[Object(s["g"])("img",{src:g.a,alt:"GitHub",width:"32",height:"auto"})],-1);Object(s["q"])();var f=u((function(e,t,n,r,o,a){return Object(s["p"])(),Object(s["d"])("div",d,[h])})),p={name:"Social"};n("508b");p.render=f,p.__scopeId="data-v-1cbbc992";var b=p,v=(n("b680"),Object(s["g"])("div",{class:"tooltip-title"},"Timestamp:",-1)),A={id:"tooltip-timestamp"},m=Object(s["g"])("div",{class:"tooltip-title"},"Response time:",-1),O={id:"tooltip-response-time"},j=Object(s["g"])("div",{class:"tooltip-title"},"Conditions:",-1),y={id:"tooltip-conditions"},w=Object(s["g"])("br",null,null,-1),x={key:0,id:"tooltip-errors-container"},T=Object(s["g"])("div",{class:"tooltip-title"},"Errors:",-1),R={id:"tooltip-errors"},I=Object(s["g"])("br",null,null,-1);function S(e,t,n,r,o,a){return Object(s["p"])(),Object(s["d"])("div",{id:"tooltip",ref:"tooltip",class:o.hidden?"invisible":"",style:"top:"+o.top+"px; left:"+o.left+"px"},[n.result?Object(s["w"])(e.$slots,"default",{key:0},(function(){return[v,Object(s["g"])("code",A,Object(s["z"])(a.prettifyTimestamp(n.result.timestamp)),1),m,Object(s["g"])("code",O,Object(s["z"])((n.result.duration/1e6).toFixed(0))+"ms",1),j,Object(s["g"])("code",y,[(Object(s["p"])(!0),Object(s["d"])(s["a"],null,Object(s["v"])(n.result.conditionResults,(function(t){return Object(s["w"])(e.$slots,"default",{key:t},(function(){return[Object(s["f"])(Object(s["z"])(t.success?"✓":"X")+" ~ "+Object(s["z"])(t.condition),1),w]}))})),128))]),n.result.errors&&n.result.errors.length?(Object(s["p"])(),Object(s["d"])("div",x,[T,Object(s["g"])("code",R,[(Object(s["p"])(!0),Object(s["d"])(s["a"],null,Object(s["v"])(n.result.errors,(function(t){return Object(s["w"])(e.$slotsLine truncated
|
||||
(function(){"use strict";var e={187:function(e,t,s){s.d(t,{L:function(){return gt}});var r=s(963),a=s(252),o=s.p+"img/logo.png";const n={class:"container container-xs relative mx-auto xl:rounded xl:border xl:shadow-xl xl:my-5 p-5 pb-12 xl:pb-5 text-left dark:bg-gray-800 dark:text-gray-200 dark:border-gray-500",id:"global"},i=(0,a.Wm)("div",{class:"mb-2"},[(0,a.Wm)("div",{class:"flex flex-wrap"},[(0,a.Wm)("div",{class:"w-3/4 text-left my-auto"},[(0,a.Wm)("div",{class:"text-3xl xl:text-5xl lg:text-4xl font-light"},"Health Status")]),(0,a.Wm)("div",{class:"w-1/4 flex justify-end"},[(0,a.Wm)("img",{src:o,alt:"Gatus",class:"object-scale-down",style:{"max-width":"100px","min-width":"50px","min-height":"50px"}})])])],-1);function l(e,t,s,r,o,l){const g=(0,a.up)("router-view"),h=(0,a.up)("Tooltip"),d=(0,a.up)("Social");return(0,a.wg)(),(0,a.j4)(a.HY,null,[(0,a.Wm)("div",n,[i,(0,a.Wm)(g,{onShowTooltip:l.showTooltip},null,8,["onShowTooltip"])]),(0,a.Wm)(h,{result:o.tooltip.result,event:o.tooltip.event},null,8,["result","event"]),(0,a.Wm)(d)],64)}var g=s.p+"img/github.png";const h=(0,a.HX)("data-v-1cbbc992");(0,a.dD)("data-v-1cbbc992");const d={id:"social"},c=(0,a.Wm)("a",{href:"https://github.com/TwinProduction/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,a.Wm)("img",{src:g,alt:"GitHub",width:"32",height:"auto"})],-1);(0,a.Cn)();const u=h(((e,t,s,r,o,n)=>((0,a.wg)(),(0,a.j4)("div",d,[c]))));var m={name:"Social"};m.render=u,m.__scopeId="data-v-1cbbc992";var p=m,A=s(577);const v=(0,a.Wm)("div",{class:"tooltip-title"},"Timestamp:",-1),f={id:"tooltip-timestamp"},w=(0,a.Wm)("div",{class:"tooltip-title"},"Response time:",-1),y={id:"tooltip-response-time"},x=(0,a.Wm)("div",{class:"tooltip-title"},"Conditions:",-1),T={id:"tooltip-conditions"},I=(0,a.Wm)("br",null,null,-1),b={key:0,id:"tooltip-errors-container"},k=(0,a.Wm)("div",{class:"tooltip-title"},"Errors:",-1),R={id:"tooltip-errors"},S=(0,a.Wm)("br",null,null,-1);function W(e,t,s,r,o,n){return(0,a.wg)(),(0,a.j4)("div",{id:"tooltip",ref:"tooltip",class:o.hidden?"invisible":"",style:"top:"+o.top+"px; left:"+o.left+"px"},[s.result?(0,a.WI)(e.$slots,"default",{key:0},(()=>[v,(0,a.Wm)("code",f,(0,A.zw)(n.prettifyTimestamp(s.result.timestamp)),1),w,(0,a.Wm)("code",y,(0,A.zw)((s.result.duration/1e6).toFixed(0))+"ms",1),x,(0,a.Wm)("code",T,[((0,a.wg)(!0),(0,a.j4)(a.HY,null,(0,a.Ko)(s.result.conditionResults,(t=>(0,a.WI)(e.$slots,"default",{key:t},(()=>[(0,a.Uk)((0,A.zw)(t.success?"✓":"X")+" ~ "+(0,A.zw)(t.condition),1),I])))),128))]),s.result.errors&&s.result.errors.length?((0,a.wg)(),(0,a.j4)("div",b,[k,(0,a.Wm)("code",R,[((0,a.wg)(!0),(0,a.j4)(a.HY,null,(0,a.Ko)(s.result.errors,(t=>(0,a.WI)(e.$slots,"default",{key:t},(()=>[(0,a.Uk)(" - "+(0,A.zw)(t),1),S])))),128))])])):(0,a.ry)("",!0)])):(0,a.ry)("",!0)],6)}var C={name:"Services",props:{event:Event,result:Object},methods:{prettifyTimestamp(e){let t=new Date(e),s=t.getFullYear(),r=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),a=(t.getDate()<10?"0":"")+t.getDate(),o=(t.getHours()<10?"0":"")+t.getHours(),n=(t.getMinutes()<10?"0":"")+t.getMinutes(),i=(t.getSeconds()<10?"0":"")+t.getSeconds();return s+"-"+r+"-"+a+" "+o+":"+n+":"+i},htmlEntities(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")},reposition(){if(this.event&&this.event.type)if("mouseenter"===this.event.type){let e=this.event.target.getBoundingClientRect().y+30,t=this.event.target.getBoundingClientRect().x,s=this.$refs.tooltip.getBoundingClientRect();t+window.scrollX+s.width+50>document.body.getBoundingClientRect().width&&(t=this.event.target.getBoundingClientRect().x-s.width+this.event.target.getBoundingClientRect().width,t<0&&(t+=-t)),e+window.scrollY+s.height+50>document.body.getBoundingClientRect().height&&e>=0&&(e=this.event.target.getBoundingClientRect().y-(s.height+10),e<0&&(e=this.event.target.getBoundingClientRect().y+30)),this.top=e,this.left=t}else"mouseleave"===this.event.type&&(this.hidden=!0)}},watch:{event:function(e){e&&e.type&&("mouseenter"===e.type?this.hidden=!1:"mouseleave"===e.type&&(this.hidden=!0))}},updated(){this.reposition()},created(){this.reposition()},data(){return{hidden:!0,top:0,left:0}}};C.render=W;var B=C,P={name:"App",components:{Social:p,Tooltip:B},methods:{showTooltip(e,t){this.tooltip={result:e,event:t}}},data(){return{tooltip:{}}}};P.render=l;var D=P,E=s(119);function H(e,t,s,r,o,n){const i=(0,a.up)("Services"),l=(0,a.up)("Pagination"),g=(0,a.up)("Settings");return(0,a.wg)(),(0,a.j4)(a.HY,null,[(0,a.Wm)(i,{serviceStatuses:o.serviceStatuses,showStatusOnHover:!0,onShowTooltip:n.showTooltip,onToggleShowAverageResponseTime:n.toggleShowAverageResponseTime,showAverageResponseTime:o.showAverageResponseTime},null,8,["serviceStatuses","onShowTooltip","onToggleShowAverageResponseTime","showAverageResponseTime"]),(0,a.Wm)(l,{onPage:n.changePage},null,8,["onPage"]),(0,a.Wm)(g,{onRefreshData:n.fetchData},null,8,["onRefreshData"])],64)}const O=(0,a.HX)("datLine truncated
|
||||
@@ -0,0 +1,7 @@
|
||||
(self["webpackChunkgatus"]=self["webpackChunkgatus"]||[]).push([[998],{2262:function(e,t,n){"use strict";n.d(t,{Fl:function(){return Fe},cE:function(){return a},X3:function(){return xe},PG:function(){return ye},dq:function(){return Ee},Jd:function(){return m},WL:function(){return Ie},qj:function(){return he},iH:function(){return ke},lk:function(){return b},Um:function(){return ve},XI:function(){return Se},sT:function(){return f},IU:function(){return _e},Vh:function(){return je},j:function(){return x},X$:function(){return _},SU:function(){return Oe}});var r=n(3577);const o=new WeakMap,i=[];let c;const s=Symbol(""),u=Symbol("");function l(e){return e&&!0===e._isEffect}function a(e,t=r.kT){l(e)&&(e=e.raw);const n=d(e,t);return t.lazy||n(),n}function f(e){e.active&&(h(e),e.options.onStop&&e.options.onStop(),e.active=!1)}let p=0;function d(e,t){const n=function(){if(!n.active)return t.scheduler?void 0:e();if(!i.includes(n)){h(n);try{return y(),i.push(n),c=n,e()}finally{i.pop(),b(),c=i[i.length-1]}}};return n.id=p++,n.allowRecurse=!!t.allowRecurse,n._isEffect=!0,n.active=!0,n.raw=e,n.deps=[],n.options=t,n}function h(e){const{deps:t}=e;if(t.length){for(let n=0;n<t.length;n++)t[n].delete(e);t.length=0}}let v=!0;const g=[];function m(){g.push(v),v=!1}function y(){g.push(v),v=!0}function b(){const e=g.pop();v=void 0===e||e}function x(e,t,n){if(!v||void 0===c)return;let r=o.get(e);r||o.set(e,r=new Map);let i=r.get(n);i||r.set(n,i=new Set),i.has(c)||(i.add(c),c.deps.push(i))}function _(e,t,n,i,l,a){const f=o.get(e);if(!f)return;const p=new Set,d=e=>{e&&e.forEach((e=>{(e!==c||e.allowRecurse)&&p.add(e)}))};if("clear"===t)f.forEach(d);else if("length"===n&&(0,r.kJ)(e))f.forEach(((e,t)=>{("length"===t||t>=i)&&d(e)}));else switch(void 0!==n&&d(f.get(n)),t){case"add":(0,r.kJ)(e)?(0,r.S0)(n)&&d(f.get("length")):(d(f.get(s)),(0,r._N)(e)&&d(f.get(u)));break;case"delete":(0,r.kJ)(e)||(d(f.get(s)),(0,r._N)(e)&&d(f.get(u)));break;case"set":(0,r._N)(e)&&d(f.get(s));break}const h=e=>{e.options.scheduler?e.options.scheduler(e):e()};p.forEach(h)}const w=(0,r.fY)("__proto__,__v_isRef,__isVue"),E=new Set(Object.getOwnPropertyNames(Symbol).map((e=>Symbol[e])).filter(r.yk)),k=R(),S=R(!1,!0),C=R(!0),A=R(!0,!0),O={};function R(e=!1,t=!1){return function(n,o,i){if("__v_isReactive"===o)return!e;if("__v_isReadonly"===o)return e;if("__v_raw"===o&&i===(e?t?fe:ae:t?le:ue).get(n))return n;const c=(0,r.kJ)(n);if(!e&&c&&(0,r.RI)(O,o))return Reflect.get(O,o,i);const s=Reflect.get(n,o,i);if((0,r.yk)(o)?E.has(o):w(o))return s;if(e||x(n,"get",o),t)return s;if(Ee(s)){const e=!c||!(0,r.S0)(o);return e?s.value:s}return(0,r.Kn)(s)?e?ge(s):he(s):s}}["includes","indexOf","lastIndexOf"].forEach((e=>{const t=Array.prototype[e];O[e]=function(...e){const n=_e(this);for(let t=0,o=this.length;t<o;t++)x(n,"get",t+"");const r=t.apply(n,e);return-1===r||!1===r?t.apply(n,e.map(_e)):r}})),["push","pop","shift","unshift","splice"].forEach((e=>{const t=Array.prototype[e];O[e]=function(...e){m();const n=t.apply(this,e);return b(),n}}));const I=j(),T=j(!0);function j(e=!1){return function(t,n,o,i){let c=t[n];if(!e&&(o=_e(o),c=_e(c),!(0,r.kJ)(t)&&Ee(c)&&!Ee(o)))return c.value=o,!0;const s=(0,r.kJ)(t)&&(0,r.S0)(n)?Number(n)<t.length:(0,r.RI)(t,n),u=Reflect.set(t,n,o,i);return t===_e(i)&&(s?(0,r.aU)(o,c)&&_(t,"set",n,o,c):_(t,"add",n,o)),u}}function P(e,t){const n=(0,r.RI)(e,t),o=e[t],i=Reflect.deleteProperty(e,t);return i&&n&&_(e,"delete",t,void 0,o),i}function F(e,t){const n=Reflect.has(e,t);return(0,r.yk)(t)&&E.has(t)||x(e,"has",t),n}function N(e){return x(e,"iterate",(0,r.kJ)(e)?"length":s),Reflect.ownKeys(e)}const $={get:k,set:I,deleteProperty:P,has:F,ownKeys:N},M={get:C,set(e,t){return!0},deleteProperty(e,t){return!0}},U=(0,r.l7)({},$,{get:S,set:T}),L=((0,r.l7)({},M,{get:A}),e=>(0,r.Kn)(e)?he(e):e),D=e=>(0,r.Kn)(e)?ge(e):e,B=e=>e,J=e=>Reflect.getPrototypeOf(e);function V(e,t,n=!1,r=!1){e=e["__v_raw"];const o=_e(e),i=_e(t);t!==i&&!n&&x(o,"get",t),!n&&x(o,"get",i);const{has:c}=J(o),s=r?B:n?D:L;return c.call(o,t)?s(e.get(t)):c.call(o,i)?s(e.get(i)):void 0}function G(e,t=!1){const n=this["__v_raw"],r=_e(n),o=_e(e);return e!==o&&!t&&x(r,"has",e),!t&&x(r,"has",o),e===o?n.has(e):n.has(e)||n.has(o)}function q(e,t=!1){return e=e["__v_raw"],!t&&x(_e(e),"iterate",s),Reflect.get(e,"size",e)}function H(e){e=_e(e);const t=_e(this),n=J(t),r=n.has.call(t,e);return r||(t.add(e),_(t,"add",e,e)),this}function K(e,t){t=_e(t);const n=_e(this),{has:o,get:i}=J(n);let c=o.call(n,e);c||(e=_e(e),c=o.call(n,e));const s=i.call(n,e);return n.set(e,t),c?(0,r.aU)(t,s)&&_(n,"set",e,t,s):_(n,"add",e,t),this}function W(e){const t=_e(this),{has:n,get:r}=J(t);let o=n.call(t,e);o||(e=_e(e),o=n.call(t,e));const i=r?r.call(t,e):void 0,c=t.delete(e);return o&&_(t,"delete",e,void 0,i),c}function z(){const e=_e(this),t=0!==e.size,n=void 0,r=e.clear();return t&&_(e,"clear",void 0,void 0,n),r}function Y(e,t){return function(n,r){const o=this,i=o["__v_raw"],c=_e(i),u=t?B:e?D:L;return!e&&x(c,"iterate",s),i.forEach(Line truncated
|
||||
/*!
|
||||
* vue-router v4.0.6
|
||||
* (c) 2021 Eduardo San Martin Morote
|
||||
* @license MIT
|
||||
*/
|
||||
const i="function"===typeof Symbol&&"symbol"===typeof Symbol.toStringTag,c=e=>i?Symbol(e):"_vr_"+e,s=c("rvlm"),u=c("rvd"),l=c("r"),a=c("rl"),f=c("rvl"),p="undefined"!==typeof window;function d(e){return e.__esModule||i&&"Module"===e[Symbol.toStringTag]}const h=Object.assign;function v(e,t){const n={};for(const r in t){const o=t[r];n[r]=Array.isArray(o)?o.map(e):e(o)}return n}let g=()=>{};const m=/\/$/,y=e=>e.replace(m,"");function b(e,t,n="/"){let r,o={},i="",c="";const s=t.indexOf("?"),u=t.indexOf("#",s>-1?s:0);return s>-1&&(r=t.slice(0,s),i=t.slice(s+1,u>-1?u:t.length),o=e(i)),u>-1&&(r=r||t.slice(0,u),c=t.slice(u,t.length)),r=A(null!=r?r:t,n),{fullPath:r+(i&&"?")+i+c,path:r,query:o,hash:c}}function x(e,t){let n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function _(e,t){return!t||e.toLowerCase().indexOf(t.toLowerCase())?e:e.slice(t.length)||"/"}function w(e,t,n){let r=t.matched.length-1,o=n.matched.length-1;return r>-1&&r===o&&E(t.matched[r],n.matched[o])&&k(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function E(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function k(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(let n in e)if(!S(e[n],t[n]))return!1;return!0}function S(e,t){return Array.isArray(e)?C(e,t):Array.isArray(t)?C(t,e):e===t}function C(e,t){return Array.isArray(t)?e.length===t.length&&e.every(((e,n)=>e===t[n])):1===e.length&&e[0]===t}function A(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/");let o,i,c=n.length-1;for(o=0;o<r.length;o++)if(i=r[o],1!==c&&"."!==i){if(".."!==i)break;c--}return n.slice(0,c).join("/")+"/"+r.slice(o-(o===r.length?1:0)).join("/")}var O,R;(function(e){e["pop"]="pop",e["push"]="push"})(O||(O={})),function(e){e["back"]="back",e["forward"]="forward",e["unknown"]=""}(R||(R={}));function I(e){if(!e)if(p){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return"/"!==e[0]&&"#"!==e[0]&&(e="/"+e),y(e)}const T=/^[^#]+#/;function j(e,t){return e.replace(T,"#")+t}function P(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const F=()=>({left:window.pageXOffset,top:window.pageYOffset});function N(e){let t;if("el"in e){let n=e.el;const r="string"===typeof n&&n.startsWith("#");0;const o="string"===typeof n?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=P(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(null!=t.left?t.left:window.pageXOffset,null!=t.top?t.top:window.pageYOffset)}function $(e,t){const n=history.state?history.state.position-t:-1;return n+e}const M=new Map;function U(e,t){M.set(e,t)}function L(e){const t=M.get(e);return M.delete(e),t}let D=()=>location.protocol+"//"+location.host;function B(e,t){const{pathname:n,search:r,hash:o}=t,i=e.indexOf("#");if(i>-1){let e=o.slice(1);return"/"!==e[0]&&(e="/"+e),_(e,"")}const c=_(n,e);return c+r+o}function J(e,t,n,r){let o=[],i=[],c=null;const s=({state:i})=>{const s=B(e,location),u=n.value,l=t.value;let a=0;if(i){if(n.value=s,t.value=i,c&&c===u)return void(c=null);a=l?i.position-l.position:0}else r(s);o.forEach((e=>{e(n.value,u,{delta:a,type:O.pop,direction:a?a>0?R.forward:R.back:R.unknown})}))};function u(){c=n.value}function l(e){o.push(e);const t=()=>{const t=o.indexOf(e);t>-1&&o.splice(t,1)};return i.push(t),t}function a(){const{history:e}=window;e.state&&e.replaceState(h({},e.state,{scroll:F()}),"")}function f(){for(const e of i)e();i=[],window.removeEventListener("popstate",s),window.removeEventListener("beforeunload",a)}return window.addEventListener("popstate",s),window.addEventListener("beforeunload",a),{pauseListeners:u,listen:l,destroy:f}}function V(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?F():null}}function G(e){const{history:t,location:n}=window;let r={value:B(e,n)},o={value:t.state};function i(r,i,c){const s=e.indexOf("#"),u=s>-1?(n.host&&document.querySelector("base")?e:e.slice(s))+r:D()+e+r;try{t[c?"replaceState":"pushState"](i,"",u),o.value=i}catch(l){console.error(l),n[c?"replace":"assign"](u)}}function c(e,n){const c=h({},t.state,V(o.value.back,e,o.value.forward,!0),n,{position:o.value.position});i(e,c,!0),r.value=e}function s(e,n){const c=h({},o.value,t.state,{forward:e,scroll:F()});i(c.current,c,!0);const s=h({},V(r.value,e,null),{position:c.position+1},n);i(e,s,!1),r.value=e}return o.value||i(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0),{location:r,state:o,push:s,replace:c}}function q(e){e=I(e);const t=G(e),n=J(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}const o=h({location:"",base:e,go:r,createHref:j.bind(null,e)},t,n);return Object.defineProperty(o,"location",{get:()=>t.location.value}),Object.definLine truncated
|
||||
@@ -1,6 +1,7 @@
|
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-vendors"],{"00ee":function(e,t,n){var r=n("b622"),o=r("toStringTag"),c={};c[o]="z",e.exports="[object z]"===String(c)},"0366":function(e,t,n){var r=n("1c0b");e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 0:return function(){return e.call(t)};case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,o){return e.call(t,n,r,o)}}return function(){return e.apply(t,arguments)}}},"06cf":function(e,t,n){var r=n("83ab"),o=n("d1e7"),c=n("5c6c"),i=n("fc6a"),s=n("c04e"),a=n("5135"),u=n("0cfb"),l=Object.getOwnPropertyDescriptor;t.f=r?l:function(e,t){if(e=i(e),t=s(t,!0),u)try{return l(e,t)}catch(n){}if(a(e,t))return c(!o.f.call(e,t),e[t])}},"0cb2":function(e,t,n){var r=n("7b0b"),o=Math.floor,c="".replace,i=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,s=/\$([$&'`]|\d{1,2})/g;e.exports=function(e,t,n,a,u,l){var f=n+e.length,p=a.length,d=s;return void 0!==u&&(u=r(u),d=i),c.call(l,d,(function(r,c){var i;switch(c.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,n);case"'":return t.slice(f);case"<":i=u[c.slice(1,-1)];break;default:var s=+c;if(0===s)return r;if(s>p){var l=o(s/10);return 0===l?r:l<=p?void 0===a[l-1]?c.charAt(1):a[l-1]+c.charAt(1):r}i=a[s-1]}return void 0===i?"":i}))}},"0cfb":function(e,t,n){var r=n("83ab"),o=n("d039"),c=n("cc12");e.exports=!r&&!o((function(){return 7!=Object.defineProperty(c("div"),"a",{get:function(){return 7}}).a}))},1148:function(e,t,n){"use strict";var r=n("a691"),o=n("1d80");e.exports="".repeat||function(e){var t=String(o(this)),n="",c=r(e);if(c<0||c==1/0)throw RangeError("Wrong number of repetitions");for(;c>0;(c>>>=1)&&(t+=t))1&c&&(n+=t);return n}},"14c3":function(e,t,n){var r=n("c6b6"),o=n("9263");e.exports=function(e,t){var n=e.exec;if("function"===typeof n){var c=n.call(e,t);if("object"!==typeof c)throw TypeError("RegExp exec method returned something other than an Object or null");return c}if("RegExp"!==r(e))throw TypeError("RegExp#exec called on incompatible receiver");return o.call(e,t)}},"19aa":function(e,t){e.exports=function(e,t,n){if(!(e instanceof t))throw TypeError("Incorrect "+(n?n+" ":"")+"invocation");return e}},"1be4":function(e,t,n){var r=n("d066");e.exports=r("document","documentElement")},"1c0b":function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(String(e)+" is not a function");return e}},"1c7e":function(e,t,n){var r=n("b622"),o=r("iterator"),c=!1;try{var i=0,s={next:function(){return{done:!!i++}},return:function(){c=!0}};s[o]=function(){return this},Array.from(s,(function(){throw 2}))}catch(a){}e.exports=function(e,t){if(!t&&!c)return!1;var n=!1;try{var r={};r[o]=function(){return{next:function(){return{done:n=!0}}}},e(r)}catch(a){}return n}},"1cdc":function(e,t,n){var r=n("342f");e.exports=/(iphone|ipod|ipad).*applewebkit/i.test(r)},"1d80":function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},"1dde":function(e,t,n){var r=n("d039"),o=n("b622"),c=n("2d00"),i=o("species");e.exports=function(e){return c>=51||!r((function(){var t=[],n=t.constructor={};return n[i]=function(){return{foo:1}},1!==t[e](Boolean).foo}))}},2266:function(e,t,n){var r=n("825a"),o=n("e95a"),c=n("50c4"),i=n("0366"),s=n("35a1"),a=n("2a62"),u=function(e,t){this.stopped=e,this.result=t};e.exports=function(e,t,n){var l,f,p,d,h,v,b,g=n&&n.that,m=!(!n||!n.AS_ENTRIES),y=!(!n||!n.IS_ITERATOR),j=!(!n||!n.INTERRUPTED),O=i(t,g,1+m+j),x=function(e){return l&&a(l),new u(!0,e)},w=function(e){return m?(r(e),j?O(e[0],e[1],x):O(e[0],e[1])):j?O(e,x):O(e)};if(y)l=e;else{if(f=s(e),"function"!=typeof f)throw TypeError("Target is not iterable");if(o(f)){for(p=0,d=c(e.length);d>p;p++)if(h=w(e[p]),h&&h instanceof u)return h;return new u(!1)}l=f.call(e)}v=l.next;while(!(b=v.call(l)).done){try{h=w(b.value)}catch(_){throw a(l),_}if("object"==typeof h&&h&&h instanceof u)return h}return new u(!1)}},"23cb":function(e,t,n){var r=n("a691"),o=Math.max,c=Math.min;e.exports=function(e,t){var n=r(e);return n<0?o(n+t,0):c(n,t)}},"23e7":function(e,t,n){var r=n("da84"),o=n("06cf").f,c=n("9112"),i=n("6eeb"),s=n("ce4e"),a=n("e893"),u=n("94ca");e.exports=function(e,t){var n,l,f,p,d,h,v=e.target,b=e.global,g=e.stat;if(l=b?r:g?r[v]||s(v,{}):(r[v]||{}).prototype,l)for(f in t){if(d=t[f],e.noTargetGet?(h=o(l,f),p=h&&h.value):p=l[f],n=u(b?f:v+(g?".":"#")+f,e.forced),!n&&void 0!==p){if(typeof d===typeof p)continue;a(d,p)}(e.sham||p&&p.sham)&&c(d,"sham",!0),i(l,f,d,e)}}},"241c":function(e,t,n){var r=n("ca84"),o=n("7839"),c=o.concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,c)}},2626:function(e,t,n){"use strict";var r=n("d066"),o=n("9bf2"),c=n("b622"),i=n("83ab"),s=c("species");e.exports=function(e){var t=r(e),n=o.f;i&&t&&!t[s]&&n(t,s,{configurable:!0,get:function(){return this}})}},"2a62":function(e,t,n){var r=n("825a");e.exports=function(e){var t=e["return"];if(void 0!==t)return r(t.cLine truncated
|
||||
(self["webpackChunkgatus"]=self["webpackChunkgatus"]||[]).push([[998],{262:function(e,t,n){"use strict";n.d(t,{Fl:function(){return je},cE:function(){return a},X3:function(){return be},PG:function(){return ye},dq:function(){return xe},Jd:function(){return v},WL:function(){return Fe},qj:function(){return he},iH:function(){return Ce},lk:function(){return _},Um:function(){return me},XI:function(){return Ee},sT:function(){return f},IU:function(){return we},Vh:function(){return Ie},j:function(){return b},X$:function(){return w},SU:function(){return Ae}});var r=n(577);const o=new WeakMap,s=[];let l;const i=Symbol(""),c=Symbol("");function u(e){return e&&!0===e._isEffect}function a(e,t=r.kT){u(e)&&(e=e.raw);const n=d(e,t);return t.lazy||n(),n}function f(e){e.active&&(h(e),e.options.onStop&&e.options.onStop(),e.active=!1)}let p=0;function d(e,t){const n=function(){if(!n.active)return t.scheduler?void 0:e();if(!s.includes(n)){h(n);try{return y(),s.push(n),l=n,e()}finally{s.pop(),_(),l=s[s.length-1]}}};return n.id=p++,n.allowRecurse=!!t.allowRecurse,n._isEffect=!0,n.active=!0,n.raw=e,n.deps=[],n.options=t,n}function h(e){const{deps:t}=e;if(t.length){for(let n=0;n<t.length;n++)t[n].delete(e);t.length=0}}let m=!0;const g=[];function v(){g.push(m),m=!1}function y(){g.push(m),m=!0}function _(){const e=g.pop();m=void 0===e||e}function b(e,t,n){if(!m||void 0===l)return;let r=o.get(e);r||o.set(e,r=new Map);let s=r.get(n);s||r.set(n,s=new Set),s.has(l)||(s.add(l),l.deps.push(s))}function w(e,t,n,s,u,a){const f=o.get(e);if(!f)return;const p=new Set,d=e=>{e&&e.forEach((e=>{(e!==l||e.allowRecurse)&&p.add(e)}))};if("clear"===t)f.forEach(d);else if("length"===n&&(0,r.kJ)(e))f.forEach(((e,t)=>{("length"===t||t>=s)&&d(e)}));else switch(void 0!==n&&d(f.get(n)),t){case"add":(0,r.kJ)(e)?(0,r.S0)(n)&&d(f.get("length")):(d(f.get(i)),(0,r._N)(e)&&d(f.get(c)));break;case"delete":(0,r.kJ)(e)||(d(f.get(i)),(0,r._N)(e)&&d(f.get(c)));break;case"set":(0,r._N)(e)&&d(f.get(i));break}const h=e=>{e.options.scheduler?e.options.scheduler(e):e()};p.forEach(h)}const k=(0,r.fY)("__proto__,__v_isRef,__isVue"),x=new Set(Object.getOwnPropertyNames(Symbol).map((e=>Symbol[e])).filter(r.yk)),C=O(),E=O(!1,!0),S=O(!0),R=O(!0,!0),A={};function O(e=!1,t=!1){return function(n,o,s){if("__v_isReactive"===o)return!e;if("__v_isReadonly"===o)return e;if("__v_raw"===o&&s===(e?t?fe:ae:t?ue:ce).get(n))return n;const l=(0,r.kJ)(n);if(!e&&l&&(0,r.RI)(A,o))return Reflect.get(A,o,s);const i=Reflect.get(n,o,s);if((0,r.yk)(o)?x.has(o):k(o))return i;if(e||b(n,"get",o),t)return i;if(xe(i)){const e=!l||!(0,r.S0)(o);return e?i.value:i}return(0,r.Kn)(i)?e?ge(i):he(i):i}}["includes","indexOf","lastIndexOf"].forEach((e=>{const t=Array.prototype[e];A[e]=function(...e){const n=we(this);for(let t=0,o=this.length;t<o;t++)b(n,"get",t+"");const r=t.apply(n,e);return-1===r||!1===r?t.apply(n,e.map(we)):r}})),["push","pop","shift","unshift","splice"].forEach((e=>{const t=Array.prototype[e];A[e]=function(...e){v();const n=t.apply(this,e);return _(),n}}));const F=I(),T=I(!0);function I(e=!1){return function(t,n,o,s){let l=t[n];if(!e&&(o=we(o),l=we(l),!(0,r.kJ)(t)&&xe(l)&&!xe(o)))return l.value=o,!0;const i=(0,r.kJ)(t)&&(0,r.S0)(n)?Number(n)<t.length:(0,r.RI)(t,n),c=Reflect.set(t,n,o,s);return t===we(s)&&(i?(0,r.aU)(o,l)&&w(t,"set",n,o,l):w(t,"add",n,o)),c}}function P(e,t){const n=(0,r.RI)(e,t),o=e[t],s=Reflect.deleteProperty(e,t);return s&&n&&w(e,"delete",t,void 0,o),s}function j(e,t){const n=Reflect.has(e,t);return(0,r.yk)(t)&&x.has(t)||b(e,"has",t),n}function $(e){return b(e,"iterate",(0,r.kJ)(e)?"length":i),Reflect.ownKeys(e)}const L={get:C,set:F,deleteProperty:P,has:j,ownKeys:$},M={get:S,set(e,t){return!0},deleteProperty(e,t){return!0}},U=(0,r.l7)({},L,{get:E,set:T}),N=((0,r.l7)({},M,{get:R}),e=>(0,r.Kn)(e)?he(e):e),J=e=>(0,r.Kn)(e)?ge(e):e,B=e=>e,D=e=>Reflect.getPrototypeOf(e);function V(e,t,n=!1,r=!1){e=e["__v_raw"];const o=we(e),s=we(t);t!==s&&!n&&b(o,"get",t),!n&&b(o,"get",s);const{has:l}=D(o),i=r?B:n?J:N;return l.call(o,t)?i(e.get(t)):l.call(o,s)?i(e.get(s)):void 0}function q(e,t=!1){const n=this["__v_raw"],r=we(n),o=we(e);return e!==o&&!t&&b(r,"has",e),!t&&b(r,"has",o),e===o?n.has(e):n.has(e)||n.has(o)}function H(e,t=!1){return e=e["__v_raw"],!t&&b(we(e),"iterate",i),Reflect.get(e,"size",e)}function G(e){e=we(e);const t=we(this),n=D(t),r=n.has.call(t,e);return r||(t.add(e),w(t,"add",e,e)),this}function K(e,t){t=we(t);const n=we(this),{has:o,get:s}=D(n);let l=o.call(n,e);l||(e=we(e),l=o.call(n,e));const i=s.call(n,e);return n.set(e,t),l?(0,r.aU)(t,i)&&w(n,"set",e,t,i):w(n,"add",e,t),this}function W(e){const t=we(this),{has:n,get:r}=D(t);let o=n.call(t,e);o||(e=we(e),o=n.call(t,e));const s=r?r.call(t,e):void 0,l=t.delete(e);return o&&w(t,"delete",e,void 0,s),l}function z(){const e=we(this),t=0!==e.size,n=void 0,r=e.clear();return t&&w(e,"clear",void 0,void 0,n),r}function X(e,t){return function(n,r){const o=this,s=o["__v_raw"],l=we(s),c=t?B:e?J:N;return!e&&b(l,"iterate",i),s.forEach(((Line truncated
|
||||
/*!
|
||||
* vue-router v4.0.5
|
||||
* vue-router v4.0.6
|
||||
* (c) 2021 Eduardo San Martin Morote
|
||||
* @license MIT
|
||||
*/const o="function"===typeof Symbol&&"symbol"===typeof Symbol.toStringTag,c=e=>o?Symbol(e):"_vr_"+e,i=c("rvlm"),s=c("rvd"),a=c("r"),u=c("rl"),l=c("rvl"),f="undefined"!==typeof window;function p(e){return e.__esModule||o&&"Module"===e[Symbol.toStringTag]}const d=Object.assign;function h(e,t){const n={};for(const r in t){const o=t[r];n[r]=Array.isArray(o)?o.map(e):e(o)}return n}let v=()=>{};const b=/\/$/,g=e=>e.replace(b,"");function m(e,t,n="/"){let r,o={},c="",i="";const s=t.indexOf("?"),a=t.indexOf("#",s>-1?s:0);return s>-1&&(r=t.slice(0,s),c=t.slice(s+1,a>-1?a:t.length),o=e(c)),a>-1&&(r=r||t.slice(0,a),i=t.slice(a,t.length)),r=S(null!=r?r:t,n),{fullPath:r+(c&&"?")+c+i,path:r,query:o,hash:i}}function y(e,t){let n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function j(e,t){return!t||e.toLowerCase().indexOf(t.toLowerCase())?e:e.slice(t.length)||"/"}function O(e,t,n){let r=t.matched.length-1,o=n.matched.length-1;return r>-1&&r===o&&x(t.matched[r],n.matched[o])&&w(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function x(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function w(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(let n in e)if(!_(e[n],t[n]))return!1;return!0}function _(e,t){return Array.isArray(e)?E(e,t):Array.isArray(t)?E(t,e):e===t}function E(e,t){return Array.isArray(t)?e.length===t.length&&e.every((e,n)=>e===t[n]):1===e.length&&e[0]===t}function S(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/");let o,c,i=n.length-1;for(o=0;o<r.length;o++)if(c=r[o],1!==i&&"."!==c){if(".."!==c)break;i--}return n.slice(0,i).join("/")+"/"+r.slice(o-(o===r.length?1:0)).join("/")}var A,C;(function(e){e["pop"]="pop",e["push"]="push"})(A||(A={})),function(e){e["back"]="back",e["forward"]="forward",e["unknown"]=""}(C||(C={}));function k(e){if(!e)if(f){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return"/"!==e[0]&&"#"!==e[0]&&(e="/"+e),g(e)}const R=/^[^#]+#/;function P(e,t){return e.replace(R,"#")+t}function T(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const F=()=>({left:window.pageXOffset,top:window.pageYOffset});function I(e){let t;if("el"in e){let n=e.el;const r="string"===typeof n&&n.startsWith("#");0;const o="string"===typeof n?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=T(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(null!=t.left?t.left:window.pageXOffset,null!=t.top?t.top:window.pageYOffset)}function M(e,t){const n=history.state?history.state.position-t:-1;return n+e}const N=new Map;function L(e,t){N.set(e,t)}function $(e){const t=N.get(e);return N.delete(e),t}let B=()=>location.protocol+"//"+location.host;function U(e,t){const{pathname:n,search:r,hash:o}=t,c=e.indexOf("#");if(c>-1){let e=o.slice(1);return"/"!==e[0]&&(e="/"+e),j(e,"")}const i=j(n,e);return i+r+o}function D(e,t,n,r){let o=[],c=[],i=null;const s=({state:c})=>{const s=U(e,location),a=n.value,u=t.value;let l=0;if(c){if(n.value=s,t.value=c,i&&i===a)return void(i=null);l=u?c.position-u.position:0}else r(s);o.forEach(e=>{e(n.value,a,{delta:l,type:A.pop,direction:l?l>0?C.forward:C.back:C.unknown})})};function a(){i=n.value}function u(e){o.push(e);const t=()=>{const t=o.indexOf(e);t>-1&&o.splice(t,1)};return c.push(t),t}function l(){const{history:e}=window;e.state&&e.replaceState(d({},e.state,{scroll:F()}),"")}function f(){for(const e of c)e();c=[],window.removeEventListener("popstate",s),window.removeEventListener("beforeunload",l)}return window.addEventListener("popstate",s),window.addEventListener("beforeunload",l),{pauseListeners:a,listen:u,destroy:f}}function V(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?F():null}}function q(e){const{history:t,location:n}=window;let r={value:U(e,n)},o={value:t.state};function c(r,c,i){const s=e.indexOf("#"),a=s>-1?(n.host&&document.querySelector("base")?e:e.slice(s))+r:B()+e+r;try{t[i?"replaceState":"pushState"](c,"",a),o.value=c}catch(u){console.error(u),n[i?"replace":"assign"](a)}}function i(e,n){const i=d({},t.state,V(o.value.back,e,o.value.forward,!0),n,{position:o.value.position});c(e,i,!0),r.value=e}function s(e,n){const i=d({},o.value,t.state,{forward:e,scroll:F()});c(i.current,i,!0);const s=d({},V(r.value,e,null),{position:i.position+1},n);c(e,s,!1),r.value=e}return o.value||c(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0),{location:r,state:o,push:s,replace:i}}function G(e){e=k(e);const t=q(e),n=D(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}const o=d({location:"",base:e,go:r,createHref:P.bind(null,e)},t,n);return Object.defineProperty(o,"location",{get:()=>t.location.value}),Object.definLine truncated
|
||||
*/
|
||||
const s="function"===typeof Symbol&&"symbol"===typeof Symbol.toStringTag,l=e=>s?Symbol(e):"_vr_"+e,i=l("rvlm"),c=l("rvd"),u=l("r"),a=l("rl"),f=l("rvl"),p="undefined"!==typeof window;function d(e){return e.__esModule||s&&"Module"===e[Symbol.toStringTag]}const h=Object.assign;function m(e,t){const n={};for(const r in t){const o=t[r];n[r]=Array.isArray(o)?o.map(e):e(o)}return n}let g=()=>{};const v=/\/$/,y=e=>e.replace(v,"");function _(e,t,n="/"){let r,o={},s="",l="";const i=t.indexOf("?"),c=t.indexOf("#",i>-1?i:0);return i>-1&&(r=t.slice(0,i),s=t.slice(i+1,c>-1?c:t.length),o=e(s)),c>-1&&(r=r||t.slice(0,c),l=t.slice(c,t.length)),r=R(null!=r?r:t,n),{fullPath:r+(s&&"?")+s+l,path:r,query:o,hash:l}}function b(e,t){let n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function w(e,t){return!t||e.toLowerCase().indexOf(t.toLowerCase())?e:e.slice(t.length)||"/"}function k(e,t,n){let r=t.matched.length-1,o=n.matched.length-1;return r>-1&&r===o&&x(t.matched[r],n.matched[o])&&C(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function x(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function C(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(let n in e)if(!E(e[n],t[n]))return!1;return!0}function E(e,t){return Array.isArray(e)?S(e,t):Array.isArray(t)?S(t,e):e===t}function S(e,t){return Array.isArray(t)?e.length===t.length&&e.every(((e,n)=>e===t[n])):1===e.length&&e[0]===t}function R(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/");let o,s,l=n.length-1;for(o=0;o<r.length;o++)if(s=r[o],1!==l&&"."!==s){if(".."!==s)break;l--}return n.slice(0,l).join("/")+"/"+r.slice(o-(o===r.length?1:0)).join("/")}var A,O;(function(e){e["pop"]="pop",e["push"]="push"})(A||(A={})),function(e){e["back"]="back",e["forward"]="forward",e["unknown"]=""}(O||(O={}));function F(e){if(!e)if(p){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return"/"!==e[0]&&"#"!==e[0]&&(e="/"+e),y(e)}const T=/^[^#]+#/;function I(e,t){return e.replace(T,"#")+t}function P(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const j=()=>({left:window.pageXOffset,top:window.pageYOffset});function $(e){let t;if("el"in e){let n=e.el;const r="string"===typeof n&&n.startsWith("#");0;const o="string"===typeof n?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=P(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(null!=t.left?t.left:window.pageXOffset,null!=t.top?t.top:window.pageYOffset)}function L(e,t){const n=history.state?history.state.position-t:-1;return n+e}const M=new Map;function U(e,t){M.set(e,t)}function N(e){const t=M.get(e);return M.delete(e),t}let J=()=>location.protocol+"//"+location.host;function B(e,t){const{pathname:n,search:r,hash:o}=t,s=e.indexOf("#");if(s>-1){let e=o.slice(1);return"/"!==e[0]&&(e="/"+e),w(e,"")}const l=w(n,e);return l+r+o}function D(e,t,n,r){let o=[],s=[],l=null;const i=({state:s})=>{const i=B(e,location),c=n.value,u=t.value;let a=0;if(s){if(n.value=i,t.value=s,l&&l===c)return void(l=null);a=u?s.position-u.position:0}else r(i);o.forEach((e=>{e(n.value,c,{delta:a,type:A.pop,direction:a?a>0?O.forward:O.back:O.unknown})}))};function c(){l=n.value}function u(e){o.push(e);const t=()=>{const t=o.indexOf(e);t>-1&&o.splice(t,1)};return s.push(t),t}function a(){const{history:e}=window;e.state&&e.replaceState(h({},e.state,{scroll:j()}),"")}function f(){for(const e of s)e();s=[],window.removeEventListener("popstate",i),window.removeEventListener("beforeunload",a)}return window.addEventListener("popstate",i),window.addEventListener("beforeunload",a),{pauseListeners:c,listen:u,destroy:f}}function V(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?j():null}}function q(e){const{history:t,location:n}=window;let r={value:B(e,n)},o={value:t.state};function s(r,s,l){const i=e.indexOf("#"),c=i>-1?(n.host&&document.querySelector("base")?e:e.slice(i))+r:J()+e+r;try{t[l?"replaceState":"pushState"](s,"",c),o.value=s}catch(u){console.error(u),n[l?"replace":"assign"](c)}}function l(e,n){const l=h({},t.state,V(o.value.back,e,o.value.forward,!0),n,{position:o.value.position});s(e,l,!0),r.value=e}function i(e,n){const l=h({},o.value,t.state,{forward:e,scroll:j()});s(l.current,l,!0);const i=h({},V(r.value,e,null),{position:l.position+1},n);s(e,i,!1),r.value=e}return o.value||s(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0),{location:r,state:o,push:i,replace:l}}function H(e){e=F(e);const t=q(e),n=D(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}const o=h({location:"",base:e,go:r,createHref:I.bind(null,e)},t,n);return Object.defineProperty(o,"location",{get:()=>t.location.value}),Object.definLine truncated
|
||||
Reference in new issue
Block a user