mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-14 11:58:04 +00:00
Merge pull request #115 from TwinProduction/reload-on-update
#29: Automatically reload on configuration file update
This commit is contained in:
commit
18420c2d60
41 changed files with 698 additions and 530 deletions
26
README.md
26
README.md
|
@ -49,6 +49,7 @@ core applications: https://status.twinnation.org/
|
||||||
- [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries)
|
- [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries)
|
||||||
- [Basic authentication](#basic-authentication)
|
- [Basic authentication](#basic-authentication)
|
||||||
- [disable-monitoring-lock](#disable-monitoring-lock)
|
- [disable-monitoring-lock](#disable-monitoring-lock)
|
||||||
|
- [Reloading configuration on the fly](#reloading-configuration-on-the-fly)
|
||||||
- [Service groups](#service-groups)
|
- [Service groups](#service-groups)
|
||||||
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
||||||
- [Uptime Badges (ALPHA)](#uptime-badges)
|
- [Uptime Badges (ALPHA)](#uptime-badges)
|
||||||
|
@ -153,6 +154,7 @@ Note that you can also use environment variables in the configuration file (e.g.
|
||||||
| `security.basic.username` | Username for Basic authentication | Required `""` |
|
| `security.basic.username` | Username for Basic authentication | Required `""` |
|
||||||
| `security.basic.password-sha512` | Password's SHA512 hash 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` |
|
| `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` | Web configuration | `{}` |
|
||||||
| `web.address` | Address to listen on | `0.0.0.0` |
|
| `web.address` | Address to listen on | `0.0.0.0` |
|
||||||
| `web.port` | Port to listen on | `8080` |
|
| `web.port` | Port to listen on | `8080` |
|
||||||
|
@ -818,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)
|
- 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
|
||||||
|
|
||||||
Service groups are used for grouping multiple services together on the dashboard.
|
Service groups are used for grouping multiple services together on the dashboard.
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package core
|
package alert
|
||||||
|
|
||||||
// Alert is the service's alert configuration
|
// Alert is the service's alert configuration
|
||||||
type Alert struct {
|
type Alert struct {
|
||||||
// Type of alert (required)
|
// Type of alert (required)
|
||||||
Type AlertType `yaml:"type"`
|
Type Type `yaml:"type"`
|
||||||
|
|
||||||
// Enabled defines whether or not the alert is enabled
|
// Enabled defines whether or not the alert is enabled
|
||||||
//
|
//
|
||||||
|
@ -67,33 +67,3 @@ func (alert Alert) IsSendingOnResolved() bool {
|
||||||
}
|
}
|
||||||
return *alert.SendOnResolved
|
return *alert.SendOnResolved
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlertType is the type of the alert.
|
|
||||||
// The value will generally be the name of the alert provider
|
|
||||||
type AlertType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// CustomAlert is the AlertType for the custom alerting provider
|
|
||||||
CustomAlert AlertType = "custom"
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
)
|
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package alert
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
31
alerting/alert/type.go
Normal file
31
alerting/alert/type.go
Normal file
|
@ -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
|
package alerting
|
||||||
|
|
||||||
import (
|
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/custom"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/discord"
|
"github.com/TwinProduction/gatus/alerting/provider/discord"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/mattermost"
|
"github.com/TwinProduction/gatus/alerting/provider/mattermost"
|
||||||
|
@ -37,3 +39,58 @@ type Config struct {
|
||||||
// Twilio is the configuration for the twilio alerting provider
|
// Twilio is the configuration for the twilio alerting provider
|
||||||
Twilio *twilio.AlertProvider `yaml:"twilio"`
|
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"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/client"
|
"github.com/TwinProduction/gatus/client"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
@ -24,7 +25,7 @@ type AlertProvider struct {
|
||||||
Placeholders map[string]map[string]string `yaml:"placeholders,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 is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *core.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
@ -33,7 +34,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// 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
|
return provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,6 +118,6 @@ func (provider *AlertProvider) Send(serviceName, alertDescription string, resolv
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *core.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ func TestAlertProvider_buildHTTPRequestWhenTriggered(t *testing.T) {
|
||||||
|
|
||||||
func TestAlertProvider_ToCustomAlertProvider(t *testing.T) {
|
func TestAlertProvider_ToCustomAlertProvider(t *testing.T) {
|
||||||
provider := AlertProvider{URL: "http://example.com"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +14,7 @@ type AlertProvider struct {
|
||||||
WebhookURL string `yaml:"webhook-url"`
|
WebhookURL string `yaml:"webhook-url"`
|
||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *core.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
@ -22,7 +23,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// 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 message, results string
|
||||||
var colorCode int
|
var colorCode int
|
||||||
if resolved {
|
if resolved {
|
||||||
|
@ -66,6 +67,6 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *core.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
|
||||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||||
provider := AlertProvider{WebhookURL: "http://example.com"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been 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) {
|
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||||
provider := AlertProvider{WebhookURL: "http://example.com"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
@ -14,7 +15,7 @@ type AlertProvider struct {
|
||||||
Insecure bool `yaml:"insecure,omitempty"`
|
Insecure bool `yaml:"insecure,omitempty"`
|
||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *core.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
@ -23,7 +24,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// 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 message string
|
||||||
var color string
|
var color string
|
||||||
if resolved {
|
if resolved {
|
||||||
|
@ -78,6 +79,6 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *core.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
|
||||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||||
provider := AlertProvider{WebhookURL: "http://example.org"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been 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) {
|
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||||
provider := AlertProvider{WebhookURL: "http://example.org"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
@ -19,7 +20,7 @@ type AlertProvider struct {
|
||||||
Recipients string `yaml:"recipients"`
|
Recipients string `yaml:"recipients"`
|
||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *core.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
@ -29,7 +30,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||||
// Reference doc for messagebird https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
// 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
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.GetDescription())
|
||||||
|
@ -53,6 +54,6 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *core.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||||
Originator: "1",
|
Originator: "1",
|
||||||
Recipients: "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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
@ -56,7 +57,7 @@ func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||||
Originator: "1",
|
Originator: "1",
|
||||||
Recipients: "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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +14,7 @@ type AlertProvider struct {
|
||||||
IntegrationKey string `yaml:"integration-key"`
|
IntegrationKey string `yaml:"integration-key"`
|
||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *core.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
@ -24,7 +25,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||||
//
|
//
|
||||||
// relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
|
// 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
|
var message, eventAction, resolveKey string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.GetDescription())
|
||||||
|
@ -55,6 +56,6 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *core.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
|
||||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||||
provider := AlertProvider{IntegrationKey: "00000000000000000000000000000000"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been 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) {
|
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||||
provider := AlertProvider{IntegrationKey: "00000000000000000000000000000000"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/discord"
|
"github.com/TwinProduction/gatus/alerting/provider/discord"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/mattermost"
|
"github.com/TwinProduction/gatus/alerting/provider/mattermost"
|
||||||
|
@ -18,14 +19,14 @@ type AlertProvider interface {
|
||||||
IsValid() bool
|
IsValid() bool
|
||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// 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 returns the provider's default alert configuration
|
||||||
GetDefaultAlert() *core.Alert
|
GetDefaultAlert() *alert.Alert
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseWithDefaultAlert parses a service alert by using the provider's default alert as a baseline
|
// ParseWithDefaultAlert parses a service alert by using the provider's default alert as a baseline
|
||||||
func ParseWithDefaultAlert(providerDefaultAlert, serviceAlert *core.Alert) {
|
func ParseWithDefaultAlert(providerDefaultAlert, serviceAlert *alert.Alert) {
|
||||||
if providerDefaultAlert == nil || serviceAlert == nil {
|
if providerDefaultAlert == nil || serviceAlert == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,13 @@ package provider
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseWithDefaultAlert(t *testing.T) {
|
func TestParseWithDefaultAlert(t *testing.T) {
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
DefaultAlert, ServiceAlert, ExpectedOutputAlert *core.Alert
|
DefaultAlert, ServiceAlert, ExpectedOutputAlert *alert.Alert
|
||||||
}
|
}
|
||||||
enabled := true
|
enabled := true
|
||||||
disabled := false
|
disabled := false
|
||||||
|
@ -18,18 +18,18 @@ func TestParseWithDefaultAlert(t *testing.T) {
|
||||||
scenarios := []Scenario{
|
scenarios := []Scenario{
|
||||||
{
|
{
|
||||||
Name: "service-alert-type-only",
|
Name: "service-alert-type-only",
|
||||||
DefaultAlert: &core.Alert{
|
DefaultAlert: &alert.Alert{
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
SendOnResolved: &enabled,
|
SendOnResolved: &enabled,
|
||||||
Description: &firstDescription,
|
Description: &firstDescription,
|
||||||
FailureThreshold: 5,
|
FailureThreshold: 5,
|
||||||
SuccessThreshold: 10,
|
SuccessThreshold: 10,
|
||||||
},
|
},
|
||||||
ServiceAlert: &core.Alert{
|
ServiceAlert: &alert.Alert{
|
||||||
Type: core.DiscordAlert,
|
Type: alert.TypeDiscord,
|
||||||
},
|
},
|
||||||
ExpectedOutputAlert: &core.Alert{
|
ExpectedOutputAlert: &alert.Alert{
|
||||||
Type: core.DiscordAlert,
|
Type: alert.TypeDiscord,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
SendOnResolved: &enabled,
|
SendOnResolved: &enabled,
|
||||||
Description: &firstDescription,
|
Description: &firstDescription,
|
||||||
|
@ -39,23 +39,23 @@ func TestParseWithDefaultAlert(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "service-alert-overwrites-default-alert",
|
Name: "service-alert-overwrites-default-alert",
|
||||||
DefaultAlert: &core.Alert{
|
DefaultAlert: &alert.Alert{
|
||||||
Enabled: &disabled,
|
Enabled: &disabled,
|
||||||
SendOnResolved: &disabled,
|
SendOnResolved: &disabled,
|
||||||
Description: &firstDescription,
|
Description: &firstDescription,
|
||||||
FailureThreshold: 5,
|
FailureThreshold: 5,
|
||||||
SuccessThreshold: 10,
|
SuccessThreshold: 10,
|
||||||
},
|
},
|
||||||
ServiceAlert: &core.Alert{
|
ServiceAlert: &alert.Alert{
|
||||||
Type: core.TelegramAlert,
|
Type: alert.TypeTelegram,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
SendOnResolved: &enabled,
|
SendOnResolved: &enabled,
|
||||||
Description: &secondDescription,
|
Description: &secondDescription,
|
||||||
FailureThreshold: 6,
|
FailureThreshold: 6,
|
||||||
SuccessThreshold: 11,
|
SuccessThreshold: 11,
|
||||||
},
|
},
|
||||||
ExpectedOutputAlert: &core.Alert{
|
ExpectedOutputAlert: &alert.Alert{
|
||||||
Type: core.TelegramAlert,
|
Type: alert.TypeTelegram,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
SendOnResolved: &enabled,
|
SendOnResolved: &enabled,
|
||||||
Description: &secondDescription,
|
Description: &secondDescription,
|
||||||
|
@ -65,22 +65,22 @@ func TestParseWithDefaultAlert(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "service-alert-partially-overwrites-default-alert",
|
Name: "service-alert-partially-overwrites-default-alert",
|
||||||
DefaultAlert: &core.Alert{
|
DefaultAlert: &alert.Alert{
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
SendOnResolved: &enabled,
|
SendOnResolved: &enabled,
|
||||||
Description: &firstDescription,
|
Description: &firstDescription,
|
||||||
FailureThreshold: 5,
|
FailureThreshold: 5,
|
||||||
SuccessThreshold: 10,
|
SuccessThreshold: 10,
|
||||||
},
|
},
|
||||||
ServiceAlert: &core.Alert{
|
ServiceAlert: &alert.Alert{
|
||||||
Type: core.DiscordAlert,
|
Type: alert.TypeDiscord,
|
||||||
Enabled: nil,
|
Enabled: nil,
|
||||||
SendOnResolved: nil,
|
SendOnResolved: nil,
|
||||||
FailureThreshold: 6,
|
FailureThreshold: 6,
|
||||||
SuccessThreshold: 11,
|
SuccessThreshold: 11,
|
||||||
},
|
},
|
||||||
ExpectedOutputAlert: &core.Alert{
|
ExpectedOutputAlert: &alert.Alert{
|
||||||
Type: core.DiscordAlert,
|
Type: alert.TypeDiscord,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
SendOnResolved: &enabled,
|
SendOnResolved: &enabled,
|
||||||
Description: &firstDescription,
|
Description: &firstDescription,
|
||||||
|
@ -90,19 +90,19 @@ func TestParseWithDefaultAlert(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "default-alert-type-should-be-ignored",
|
Name: "default-alert-type-should-be-ignored",
|
||||||
DefaultAlert: &core.Alert{
|
DefaultAlert: &alert.Alert{
|
||||||
Type: core.TelegramAlert,
|
Type: alert.TypeTelegram,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
SendOnResolved: &enabled,
|
SendOnResolved: &enabled,
|
||||||
Description: &firstDescription,
|
Description: &firstDescription,
|
||||||
FailureThreshold: 5,
|
FailureThreshold: 5,
|
||||||
SuccessThreshold: 10,
|
SuccessThreshold: 10,
|
||||||
},
|
},
|
||||||
ServiceAlert: &core.Alert{
|
ServiceAlert: &alert.Alert{
|
||||||
Type: core.DiscordAlert,
|
Type: alert.TypeDiscord,
|
||||||
},
|
},
|
||||||
ExpectedOutputAlert: &core.Alert{
|
ExpectedOutputAlert: &alert.Alert{
|
||||||
Type: core.DiscordAlert,
|
Type: alert.TypeDiscord,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
SendOnResolved: &enabled,
|
SendOnResolved: &enabled,
|
||||||
Description: &firstDescription,
|
Description: &firstDescription,
|
||||||
|
@ -112,8 +112,8 @@ func TestParseWithDefaultAlert(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "no-default-alert",
|
Name: "no-default-alert",
|
||||||
DefaultAlert: &core.Alert{
|
DefaultAlert: &alert.Alert{
|
||||||
Type: core.DiscordAlert,
|
Type: alert.TypeDiscord,
|
||||||
Enabled: nil,
|
Enabled: nil,
|
||||||
SendOnResolved: nil,
|
SendOnResolved: nil,
|
||||||
Description: &firstDescription,
|
Description: &firstDescription,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +14,7 @@ type AlertProvider struct {
|
||||||
WebhookURL string `yaml:"webhook-url"` // Slack webhook URL
|
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 is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *core.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
@ -22,7 +23,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// 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
|
var message, color, results string
|
||||||
if resolved {
|
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)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", service.Name, alert.SuccessThreshold)
|
||||||
|
@ -66,6 +67,6 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *core.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
|
||||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||||
provider := AlertProvider{WebhookURL: "http://example.com"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been 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) {
|
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||||
provider := AlertProvider{WebhookURL: "http://example.com"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
@ -14,7 +15,7 @@ type AlertProvider struct {
|
||||||
ID string `yaml:"id"`
|
ID string `yaml:"id"`
|
||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *core.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
@ -23,7 +24,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// 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 message, results string
|
||||||
if resolved {
|
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)
|
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)
|
||||||
|
@ -54,6 +55,6 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *core.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
|
||||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||||
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
@ -47,7 +48,7 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||||
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"}
|
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"}
|
||||||
description := "Healthcheck Successful"
|
description := "Healthcheck Successful"
|
||||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{Description: &description}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.Alert{Description: &description}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||||
if customAlertProvider == nil {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
@ -69,7 +70,7 @@ func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||||
|
|
||||||
func TestAlertProvider_ToCustomAlertProviderWithDescription(t *testing.T) {
|
func TestAlertProvider_ToCustomAlertProviderWithDescription(t *testing.T) {
|
||||||
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"}
|
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 {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
@ -18,7 +19,7 @@ type AlertProvider struct {
|
||||||
To string `yaml:"to"`
|
To string `yaml:"to"`
|
||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *core.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
@ -27,7 +28,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// 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
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.GetDescription())
|
||||||
|
@ -50,6 +51,6 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *core.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||||
To: "4",
|
To: "4",
|
||||||
}
|
}
|
||||||
description := "alert-description"
|
description := "alert-description"
|
||||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{Name: "service-name"}, &core.Alert{Description: &description}, &core.Result{}, true)
|
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{Name: "service-name"}, &alert.Alert{Description: &description}, &core.Result{}, true)
|
||||||
if customAlertProvider == nil {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
@ -58,7 +59,7 @@ func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||||
To: "1",
|
To: "1",
|
||||||
}
|
}
|
||||||
description := "alert-description"
|
description := "alert-description"
|
||||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{Name: "service-name"}, &core.Alert{Description: &description}, &core.Result{}, false)
|
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{Name: "service-name"}, &alert.Alert{Description: &description}, &core.Result{}, false)
|
||||||
if customAlertProvider == nil {
|
if customAlertProvider == nil {
|
||||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||||
}
|
}
|
||||||
|
|
201
config/config.go
201
config/config.go
|
@ -5,8 +5,10 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/alerting"
|
"github.com/TwinProduction/gatus/alerting"
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider"
|
"github.com/TwinProduction/gatus/alerting/provider"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
"github.com/TwinProduction/gatus/k8s"
|
"github.com/TwinProduction/gatus/k8s"
|
||||||
|
@ -39,13 +41,8 @@ var (
|
||||||
// ErrConfigFileNotFound is an error returned when the configuration file could not be found
|
// ErrConfigFileNotFound is an error returned when the configuration file could not be found
|
||||||
ErrConfigFileNotFound = errors.New("configuration file not 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 is an error returned when the security configuration is invalid
|
||||||
ErrInvalidSecurityConfig = errors.New("invalid security configuration")
|
ErrInvalidSecurityConfig = errors.New("invalid security configuration")
|
||||||
|
|
||||||
config *Config
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the main configuration structure
|
// Config is the main configuration structure
|
||||||
|
@ -56,6 +53,10 @@ type Config struct {
|
||||||
// Metrics Whether to expose metrics at /metrics
|
// Metrics Whether to expose metrics at /metrics
|
||||||
Metrics bool `yaml:"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
|
// DisableMonitoringLock Whether to disable the monitoring lock
|
||||||
// The monitoring lock is what prevents multiple services from being processed at the same time.
|
// The monitoring lock is what prevents multiple services from being processed at the same time.
|
||||||
// Disabling this may lead to inaccurate response times
|
// Disabling this may lead to inaccurate response times
|
||||||
|
@ -78,47 +79,57 @@ type Config struct {
|
||||||
|
|
||||||
// Web is the configuration for the web listener
|
// Web is the configuration for the web listener
|
||||||
Web *WebConfig `yaml:"web"`
|
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
|
// HasLoadedConfigurationFileBeenModified returns whether the file that the
|
||||||
func Get() *Config {
|
// configuration has been loaded from has been modified since it was last read
|
||||||
if config == nil {
|
func (config Config) HasLoadedConfigurationFileBeenModified() bool {
|
||||||
panic(ErrConfigNotLoaded)
|
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
|
// UpdateLastFileModTime refreshes Config.lastFileModTime
|
||||||
// Used only for testing
|
func (config *Config) UpdateLastFileModTime() {
|
||||||
func Set(cfg *Config) {
|
if fileInfo, err := os.Stat(config.filePath); err == nil {
|
||||||
config = cfg
|
if !fileInfo.ModTime().IsZero() {
|
||||||
|
config.lastFileModTime = fileInfo.ModTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads a custom configuration file
|
// Load loads a custom configuration file
|
||||||
// Note that the misconfiguration of some fields may lead to panics. This is on purpose.
|
// 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)
|
log.Printf("[config][Load] Reading configuration from configFile=%s", configFile)
|
||||||
cfg, err := readConfigurationFile(configFile)
|
cfg, err := readConfigurationFile(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return ErrConfigFileNotFound
|
return nil, ErrConfigFileNotFound
|
||||||
}
|
}
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
config = cfg
|
cfg.filePath = configFile
|
||||||
return nil
|
cfg.UpdateLastFileModTime()
|
||||||
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDefaultConfiguration loads the default configuration file
|
// LoadDefaultConfiguration loads the default configuration file
|
||||||
func LoadDefaultConfiguration() error {
|
func LoadDefaultConfiguration() (*Config, error) {
|
||||||
err := Load(DefaultConfigurationFilePath)
|
cfg, err := Load(DefaultConfigurationFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == ErrConfigFileNotFound {
|
if err == ErrConfigFileNotFound {
|
||||||
return Load(DefaultFallbackConfigurationFilePath)
|
return Load(DefaultFallbackConfigurationFilePath)
|
||||||
}
|
}
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readConfigurationFile(fileName string) (config *Config, err error) {
|
func readConfigurationFile(fileName string) (config *Config, err error) {
|
||||||
|
@ -144,23 +155,33 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
||||||
} else {
|
} else {
|
||||||
// Note that the functions below may panic, and this is on purpose to prevent Gatus from starting with
|
// Note that the functions below may panic, and this is on purpose to prevent Gatus from starting with
|
||||||
// invalid configurations
|
// invalid configurations
|
||||||
validateAlertingConfig(config)
|
validateAlertingConfig(config.Alerting, config.Services, config.Debug)
|
||||||
validateSecurityConfig(config)
|
if err := validateSecurityConfig(config); err != nil {
|
||||||
validateServicesConfig(config)
|
return nil, err
|
||||||
validateKubernetesConfig(config)
|
}
|
||||||
validateWebConfig(config)
|
if err := validateServicesConfig(config); err != nil {
|
||||||
validateStorageConfig(config)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateStorageConfig(config *Config) {
|
func validateStorageConfig(config *Config) error {
|
||||||
if config.Storage == nil {
|
if config.Storage == nil {
|
||||||
config.Storage = &storage.Config{}
|
config.Storage = &storage.Config{}
|
||||||
}
|
}
|
||||||
err := storage.Initialize(config.Storage)
|
err := storage.Initialize(config.Storage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
// Remove all ServiceStatus that represent services which no longer exist in the configuration
|
// Remove all ServiceStatus that represent services which no longer exist in the configuration
|
||||||
var keys []string
|
var keys []string
|
||||||
|
@ -171,44 +192,52 @@ func validateStorageConfig(config *Config) {
|
||||||
if numberOfServiceStatusesDeleted > 0 {
|
if numberOfServiceStatusesDeleted > 0 {
|
||||||
log.Printf("[config][validateStorageConfig] Deleted %d service statuses because their matching services no longer existed", numberOfServiceStatusesDeleted)
|
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 {
|
if config.Web == nil {
|
||||||
config.Web = &WebConfig{Address: DefaultAddress, Port: DefaultPort}
|
config.Web = &WebConfig{Address: DefaultAddress, Port: DefaultPort}
|
||||||
} else {
|
} 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 != nil && config.Kubernetes.AutoDiscover {
|
||||||
if config.Kubernetes.ServiceTemplate == nil {
|
if config.Kubernetes.ServiceTemplate == nil {
|
||||||
panic("kubernetes.service-template cannot be nil")
|
return errors.New("kubernetes.service-template cannot be nil")
|
||||||
}
|
}
|
||||||
if config.Debug {
|
if config.Debug {
|
||||||
log.Println("[config][validateKubernetesConfig] Automatically discovering Kubernetes services...")
|
log.Println("[config][validateKubernetesConfig] Automatically discovering Kubernetes services...")
|
||||||
}
|
}
|
||||||
discoveredServices, err := k8s.DiscoverServices(config.Kubernetes)
|
discoveredServices, err := k8s.DiscoverServices(config.Kubernetes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
config.Services = append(config.Services, discoveredServices...)
|
config.Services = append(config.Services, discoveredServices...)
|
||||||
log.Printf("[config][validateKubernetesConfig] Discovered %d Kubernetes services", len(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 {
|
for _, service := range config.Services {
|
||||||
if config.Debug {
|
if config.Debug {
|
||||||
log.Printf("[config][validateServicesConfig] Validating service '%s'", service.Name)
|
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))
|
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 != nil {
|
||||||
if config.Security.IsValid() {
|
if config.Security.IsValid() {
|
||||||
if config.Debug {
|
if config.Debug {
|
||||||
|
@ -217,44 +246,45 @@ func validateSecurityConfig(config *Config) {
|
||||||
} else {
|
} else {
|
||||||
// If there was an attempt to configure security, then it must mean that some confidential or private
|
// 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.
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateAlertingConfig validates the alerting configuration
|
// validateAlertingConfig validates the alerting configuration
|
||||||
// Note that the alerting configuration has to be validated before the service configuration, because the default alert
|
// 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()
|
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before core.Service.ValidateAndSetDefaults()
|
||||||
// sets the default alert values when none are set.
|
// sets the default alert values when none are set.
|
||||||
func validateAlertingConfig(config *Config) {
|
func validateAlertingConfig(alertingConfig *alerting.Config, services []*core.Service, debug bool) {
|
||||||
if config.Alerting == nil {
|
if alertingConfig == nil {
|
||||||
log.Printf("[config][validateAlertingConfig] Alerting is not configured")
|
log.Printf("[config][validateAlertingConfig] Alerting is not configured")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alertTypes := []core.AlertType{
|
alertTypes := []alert.Type{
|
||||||
core.CustomAlert,
|
alert.TypeCustom,
|
||||||
core.DiscordAlert,
|
alert.TypeDiscord,
|
||||||
core.MattermostAlert,
|
alert.TypeMattermost,
|
||||||
core.MessagebirdAlert,
|
alert.TypeMessagebird,
|
||||||
core.PagerDutyAlert,
|
alert.TypePagerDuty,
|
||||||
core.SlackAlert,
|
alert.TypeSlack,
|
||||||
core.TelegramAlert,
|
alert.TypeTelegram,
|
||||||
core.TwilioAlert,
|
alert.TypeTwilio,
|
||||||
}
|
}
|
||||||
var validProviders, invalidProviders []core.AlertType
|
var validProviders, invalidProviders []alert.Type
|
||||||
for _, alertType := range alertTypes {
|
for _, alertType := range alertTypes {
|
||||||
alertProvider := GetAlertingProviderByAlertType(config, alertType)
|
alertProvider := alertingConfig.GetAlertingProviderByAlertType(alertType)
|
||||||
if alertProvider != nil {
|
if alertProvider != nil {
|
||||||
if alertProvider.IsValid() {
|
if alertProvider.IsValid() {
|
||||||
// Parse alerts with the provider's default alert
|
// Parse alerts with the provider's default alert
|
||||||
if alertProvider.GetDefaultAlert() != nil {
|
if alertProvider.GetDefaultAlert() != nil {
|
||||||
for _, service := range config.Services {
|
for _, service := range services {
|
||||||
for alertIndex, alert := range service.Alerts {
|
for alertIndex, serviceAlert := range service.Alerts {
|
||||||
if alertType == alert.Type {
|
if alertType == serviceAlert.Type {
|
||||||
if config.Debug {
|
if debug {
|
||||||
log.Printf("[config][validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in service=%s", alertIndex, alertType, service.Name)
|
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(), alert)
|
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), serviceAlert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,58 +300,3 @@ func validateAlertingConfig(config *Config) {
|
||||||
}
|
}
|
||||||
log.Printf("[config][validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders)
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/alerting"
|
"github.com/TwinProduction/gatus/alerting"
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/discord"
|
"github.com/TwinProduction/gatus/alerting/provider/discord"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/mattermost"
|
"github.com/TwinProduction/gatus/alerting/provider/mattermost"
|
||||||
|
@ -20,31 +21,15 @@ import (
|
||||||
v1 "k8s.io/api/core/v1"
|
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) {
|
func TestLoadFileThatDoesNotExist(t *testing.T) {
|
||||||
err := Load("file-that-does-not-exist.yaml")
|
_, err := Load("file-that-does-not-exist.yaml")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Should've returned an error, because the file specified doesn't exist")
|
t.Error("Should've returned an error, because the file specified doesn't exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDefaultConfigurationFile(t *testing.T) {
|
func TestLoadDefaultConfigurationFile(t *testing.T) {
|
||||||
err := LoadDefaultConfiguration()
|
_, err := LoadDefaultConfiguration()
|
||||||
if err == nil {
|
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")
|
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) {
|
func TestParseAndValidateConfigBytesWithInvalidPort(t *testing.T) {
|
||||||
defer func() { recover() }()
|
_, err := parseAndValidateConfigBytes([]byte(`
|
||||||
_, _ = parseAndValidateConfigBytes([]byte(`
|
|
||||||
web:
|
web:
|
||||||
port: 65536
|
port: 65536
|
||||||
address: 127.0.0.1
|
address: 127.0.0.1
|
||||||
|
@ -247,7 +231,9 @@ services:
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[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) {
|
func TestParseAndValidateConfigBytesWithMetricsAndCustomUserAgentHeader(t *testing.T) {
|
||||||
|
@ -419,8 +405,8 @@ services:
|
||||||
t.Fatal("There should've been 7 alerts configured")
|
t.Fatal("There should've been 7 alerts configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Services[0].Alerts[0].Type != core.SlackAlert {
|
if config.Services[0].Alerts[0].Type != alert.TypeSlack {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.SlackAlert, config.Services[0].Alerts[0].Type)
|
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() {
|
if !config.Services[0].Alerts[0].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -432,8 +418,8 @@ 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)
|
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 {
|
if config.Services[0].Alerts[1].Type != alert.TypePagerDuty {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.PagerDutyAlert, config.Services[0].Alerts[1].Type)
|
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" {
|
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())
|
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())
|
||||||
|
@ -445,8 +431,8 @@ services:
|
||||||
t.Errorf("The success threshold of the alert should've been %d, but it was %d", 5, config.Services[0].Alerts[1].SuccessThreshold)
|
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.MattermostAlert {
|
if config.Services[0].Alerts[2].Type != alert.TypeMattermost {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.MattermostAlert, config.Services[0].Alerts[2].Type)
|
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() {
|
if !config.Services[0].Alerts[2].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -458,15 +444,15 @@ services:
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[2].SuccessThreshold)
|
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.MessagebirdAlert {
|
if config.Services[0].Alerts[3].Type != alert.TypeMessagebird {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.MessagebirdAlert, config.Services[0].Alerts[3].Type)
|
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() {
|
if config.Services[0].Alerts[3].IsEnabled() {
|
||||||
t.Error("The alert should've been disabled")
|
t.Error("The alert should've been disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Services[0].Alerts[4].Type != core.DiscordAlert {
|
if config.Services[0].Alerts[4].Type != alert.TypeDiscord {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.DiscordAlert, config.Services[0].Alerts[4].Type)
|
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() {
|
if !config.Services[0].Alerts[4].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -478,8 +464,8 @@ services:
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[4].SuccessThreshold)
|
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 != core.TelegramAlert {
|
if config.Services[0].Alerts[5].Type != alert.TypeTelegram {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.TelegramAlert, config.Services[0].Alerts[5].Type)
|
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() {
|
if !config.Services[0].Alerts[5].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -491,8 +477,8 @@ services:
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[5].SuccessThreshold)
|
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 != core.TwilioAlert {
|
if config.Services[0].Alerts[6].Type != alert.TypeTwilio {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.TwilioAlert, config.Services[0].Alerts[6].Type)
|
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() {
|
if !config.Services[0].Alerts[6].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -633,7 +619,7 @@ services:
|
||||||
if config.Alerting.Discord.WebhookURL != "http://example.org" {
|
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)
|
t.Errorf("Discord webhook should've been %s, but was %s", "http://example.org", config.Alerting.Discord.WebhookURL)
|
||||||
}
|
}
|
||||||
if GetAlertingProviderByAlertType(config, core.DiscordAlert) != config.Alerting.Discord {
|
if config.Alerting.GetAlertingProviderByAlertType(alert.TypeDiscord) != config.Alerting.Discord {
|
||||||
t.Error("expected discord configuration")
|
t.Error("expected discord configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -670,8 +656,8 @@ services:
|
||||||
t.Fatal("There should've been 7 alerts configured")
|
t.Fatal("There should've been 7 alerts configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Services[0].Alerts[0].Type != core.SlackAlert {
|
if config.Services[0].Alerts[0].Type != alert.TypeSlack {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.SlackAlert, config.Services[0].Alerts[0].Type)
|
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() {
|
if !config.Services[0].Alerts[0].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -683,8 +669,8 @@ 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)
|
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 {
|
if config.Services[0].Alerts[1].Type != alert.TypePagerDuty {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.PagerDutyAlert, config.Services[0].Alerts[1].Type)
|
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() != "default 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())
|
t.Errorf("The description of the alert should've been %s, but it was %s", "default description", config.Services[0].Alerts[1].GetDescription())
|
||||||
|
@ -696,8 +682,8 @@ services:
|
||||||
t.Errorf("The success threshold of the alert should've been %d, but it was %d", 5, config.Services[0].Alerts[1].SuccessThreshold)
|
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.MattermostAlert {
|
if config.Services[0].Alerts[2].Type != alert.TypeMattermost {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.MattermostAlert, config.Services[0].Alerts[2].Type)
|
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() {
|
if !config.Services[0].Alerts[2].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -709,8 +695,8 @@ services:
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[2].SuccessThreshold)
|
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.MessagebirdAlert {
|
if config.Services[0].Alerts[3].Type != alert.TypeMessagebird {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.MessagebirdAlert, config.Services[0].Alerts[3].Type)
|
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() {
|
if config.Services[0].Alerts[3].IsEnabled() {
|
||||||
t.Error("The alert should've been disabled")
|
t.Error("The alert should've been disabled")
|
||||||
|
@ -719,8 +705,8 @@ services:
|
||||||
t.Error("The alert should be sending on resolve")
|
t.Error("The alert should be sending on resolve")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Services[0].Alerts[4].Type != core.DiscordAlert {
|
if config.Services[0].Alerts[4].Type != alert.TypeDiscord {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.DiscordAlert, config.Services[0].Alerts[4].Type)
|
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() {
|
if !config.Services[0].Alerts[4].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -732,8 +718,8 @@ services:
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[4].SuccessThreshold)
|
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 != core.TelegramAlert {
|
if config.Services[0].Alerts[5].Type != alert.TypeTelegram {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.TelegramAlert, config.Services[0].Alerts[5].Type)
|
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() {
|
if !config.Services[0].Alerts[5].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -745,8 +731,8 @@ services:
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[5].SuccessThreshold)
|
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 != core.TwilioAlert {
|
if config.Services[0].Alerts[6].Type != alert.TypeTwilio {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.TwilioAlert, config.Services[0].Alerts[6].Type)
|
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() {
|
if !config.Services[0].Alerts[6].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -800,14 +786,14 @@ services:
|
||||||
if len(config.Services) != 1 {
|
if len(config.Services) != 1 {
|
||||||
t.Error("There should've been 2 services")
|
t.Error("There should've been 2 services")
|
||||||
}
|
}
|
||||||
if config.Services[0].Alerts[0].Type != core.SlackAlert {
|
if config.Services[0].Alerts[0].Type != alert.TypeSlack {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.SlackAlert, config.Services[0].Alerts[0].Type)
|
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 != core.SlackAlert {
|
if config.Services[0].Alerts[1].Type != alert.TypeSlack {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.SlackAlert, config.Services[0].Alerts[1].Type)
|
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 != core.SlackAlert {
|
if config.Services[0].Alerts[2].Type != alert.TypeSlack {
|
||||||
t.Errorf("The type of the alert should've been %s, but it was %s", core.SlackAlert, config.Services[0].Alerts[2].Type)
|
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() {
|
if !config.Services[0].Alerts[0].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
|
@ -1043,8 +1029,7 @@ services:
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithInvalidSecurityConfig(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithInvalidSecurityConfig(t *testing.T) {
|
||||||
defer func() { recover() }()
|
_, err := parseAndValidateConfigBytes([]byte(`
|
||||||
_, _ = parseAndValidateConfigBytes([]byte(`
|
|
||||||
security:
|
security:
|
||||||
basic:
|
basic:
|
||||||
username: "admin"
|
username: "admin"
|
||||||
|
@ -1055,7 +1040,9 @@ services:
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
`))
|
`))
|
||||||
t.Error("Function should've panicked")
|
if err == nil {
|
||||||
|
t.Error("Function should've returned an error")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithValidSecurityConfig(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithValidSecurityConfig(t *testing.T) {
|
||||||
|
@ -1176,8 +1163,7 @@ kubernetes:
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithKubernetesAutoDiscoveryButNoServiceTemplate(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithKubernetesAutoDiscoveryButNoServiceTemplate(t *testing.T) {
|
||||||
defer func() { recover() }()
|
_, err := parseAndValidateConfigBytes([]byte(`
|
||||||
_, _ = parseAndValidateConfigBytes([]byte(`
|
|
||||||
kubernetes:
|
kubernetes:
|
||||||
cluster-mode: "mock"
|
cluster-mode: "mock"
|
||||||
auto-discover: true
|
auto-discover: true
|
||||||
|
@ -1186,12 +1172,13 @@ kubernetes:
|
||||||
hostname-suffix: ".default.svc.cluster.local"
|
hostname-suffix: ".default.svc.cluster.local"
|
||||||
target-path: "/health"
|
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) {
|
func TestParseAndValidateConfigBytesWithKubernetesAutoDiscoveryUsingClusterModeIn(t *testing.T) {
|
||||||
defer func() { recover() }()
|
_, err := parseAndValidateConfigBytes([]byte(`
|
||||||
_, _ = parseAndValidateConfigBytes([]byte(`
|
|
||||||
kubernetes:
|
kubernetes:
|
||||||
cluster-mode: "in"
|
cluster-mode: "in"
|
||||||
auto-discover: true
|
auto-discover: true
|
||||||
|
@ -1204,45 +1191,44 @@ kubernetes:
|
||||||
hostname-suffix: ".default.svc.cluster.local"
|
hostname-suffix: ".default.svc.cluster.local"
|
||||||
target-path: "/health"
|
target-path: "/health"
|
||||||
`))
|
`))
|
||||||
// TODO: find a way to test this?
|
if err == nil {
|
||||||
t.Error("Function should've panicked because testing with ClusterModeIn isn't supported")
|
t.Error("Function should've returned an error because testing with ClusterModeIn isn't supported")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAlertingProviderByAlertType(t *testing.T) {
|
func TestGetAlertingProviderByAlertType(t *testing.T) {
|
||||||
cfg := &Config{
|
alertingConfig := &alerting.Config{
|
||||||
Alerting: &alerting.Config{
|
Custom: &custom.AlertProvider{},
|
||||||
Custom: &custom.AlertProvider{},
|
Discord: &discord.AlertProvider{},
|
||||||
Discord: &discord.AlertProvider{},
|
Mattermost: &mattermost.AlertProvider{},
|
||||||
Mattermost: &mattermost.AlertProvider{},
|
Messagebird: &messagebird.AlertProvider{},
|
||||||
Messagebird: &messagebird.AlertProvider{},
|
PagerDuty: &pagerduty.AlertProvider{},
|
||||||
PagerDuty: &pagerduty.AlertProvider{},
|
Slack: &slack.AlertProvider{},
|
||||||
Slack: &slack.AlertProvider{},
|
Telegram: &telegram.AlertProvider{},
|
||||||
Telegram: &telegram.AlertProvider{},
|
Twilio: &twilio.AlertProvider{},
|
||||||
Twilio: &twilio.AlertProvider{},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
if GetAlertingProviderByAlertType(cfg, core.CustomAlert) != cfg.Alerting.Custom {
|
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeCustom) != alertingConfig.Custom {
|
||||||
t.Error("expected Custom configuration")
|
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")
|
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")
|
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")
|
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")
|
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")
|
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")
|
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")
|
t.Error("expected Twilio configuration")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ type WebConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateAndSetDefaults checks and sets the default values for fields that are not set
|
// 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
|
// Validate the Address
|
||||||
if len(web.Address) == 0 {
|
if len(web.Address) == 0 {
|
||||||
web.Address = DefaultAddress
|
web.Address = DefaultAddress
|
||||||
|
@ -25,8 +25,9 @@ func (web *WebConfig) validateAndSetDefaults() {
|
||||||
if web.Port == 0 {
|
if web.Port == 0 {
|
||||||
web.Port = DefaultPort
|
web.Port = DefaultPort
|
||||||
} else if web.Port < 0 || web.Port > math.MaxUint16 {
|
} 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
|
// SocketAddress returns the combination of the Address and the Port
|
||||||
|
|
|
@ -44,20 +44,19 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle creates the router and starts the server
|
// Handle creates the router and starts the server
|
||||||
func Handle() {
|
func Handle(securityConfig *security.Config, webConfig *config.WebConfig, enableMetrics bool) {
|
||||||
cfg := config.Get()
|
var router http.Handler = CreateRouter(securityConfig, enableMetrics)
|
||||||
var router http.Handler = CreateRouter(cfg)
|
|
||||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||||
router = developmentCorsHandler(router)
|
router = developmentCorsHandler(router)
|
||||||
}
|
}
|
||||||
server = &http.Server{
|
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,
|
Handler: router,
|
||||||
ReadTimeout: 15 * time.Second,
|
ReadTimeout: 15 * time.Second,
|
||||||
WriteTimeout: 15 * time.Second,
|
WriteTimeout: 15 * time.Second,
|
||||||
IdleTimeout: 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" {
|
if os.Getenv("ROUTER_TEST") == "true" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -73,15 +72,15 @@ func Shutdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRouter creates the router for the http server
|
// 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()
|
router := mux.NewRouter()
|
||||||
if cfg.Metrics {
|
if enabledMetrics {
|
||||||
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
||||||
}
|
}
|
||||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||||
router.HandleFunc("/favicon.ico", favIconHandler).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", secureIfNecessary(securityConfig, 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/{key}", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
||||||
router.HandleFunc("/api/v1/badges/uptime/{duration}/{identifier}", badgeHandler).Methods("GET")
|
router.HandleFunc("/api/v1/badges/uptime/{duration}/{identifier}", badgeHandler).Methods("GET")
|
||||||
// SPA
|
// SPA
|
||||||
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
|
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
|
||||||
|
@ -90,9 +89,9 @@ func CreateRouter(cfg *config.Config) *mux.Router {
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
func secureIfNecessary(cfg *config.Config, handler http.HandlerFunc) http.HandlerFunc {
|
func secureIfNecessary(securityConfig *security.Config, handler http.HandlerFunc) http.HandlerFunc {
|
||||||
if cfg.Security != nil && cfg.Security.IsValid() {
|
if securityConfig != nil && securityConfig.IsValid() {
|
||||||
return security.Handler(handler, cfg.Security)
|
return security.Handler(handler, securityConfig)
|
||||||
}
|
}
|
||||||
return handler
|
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[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()})
|
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 {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
Path 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("ROUTER_TEST", "true")
|
||||||
_ = os.Setenv("ENVIRONMENT", "dev")
|
_ = os.Setenv("ENVIRONMENT", "dev")
|
||||||
defer os.Clearenv()
|
defer os.Clearenv()
|
||||||
Handle()
|
Handle(cfg.Security, cfg.Web, cfg.Metrics)
|
||||||
defer Shutdown()
|
defer Shutdown()
|
||||||
request, _ := http.NewRequest("GET", "/health", nil)
|
request, _ := http.NewRequest("GET", "/health", nil)
|
||||||
responseRecorder := httptest.NewRecorder()
|
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
|
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
|
||||||
firstResult.Timestamp = time.Time{}
|
firstResult.Timestamp = time.Time{}
|
||||||
secondResult.Timestamp = time.Time{}
|
secondResult.Timestamp = time.Time{}
|
||||||
router := CreateRouter(&config.Config{})
|
router := CreateRouter(nil, false)
|
||||||
|
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
|
@ -29,16 +29,17 @@ type DNS struct {
|
||||||
QueryName string `yaml:"query-name"`
|
QueryName string `yaml:"query-name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) validateAndSetDefault() {
|
func (d *DNS) validateAndSetDefault() error {
|
||||||
if len(d.QueryName) == 0 {
|
if len(d.QueryName) == 0 {
|
||||||
panic(ErrDNSWithNoQueryName)
|
return ErrDNSWithNoQueryName
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(d.QueryName, ".") {
|
if !strings.HasSuffix(d.QueryName, ".") {
|
||||||
d.QueryName += "."
|
d.QueryName += "."
|
||||||
}
|
}
|
||||||
if _, ok := dns.StringToType[d.QueryType]; !ok {
|
if _, ok := dns.StringToType[d.QueryType]; !ok {
|
||||||
panic(ErrDNSWithInvalidQueryType)
|
return ErrDNSWithInvalidQueryType
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) query(url string, result *Result) {
|
func (d *DNS) query(url string, result *Result) {
|
||||||
|
|
|
@ -109,8 +109,10 @@ func TestService_ValidateAndSetDefaultsWithNoDNSQueryName(t *testing.T) {
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "",
|
QueryName: "",
|
||||||
}
|
}
|
||||||
dns.validateAndSetDefault()
|
err := dns.validateAndSetDefault()
|
||||||
t.Fatal("Should've panicked because service`s dns didn't have a query name, which is a mandatory field for dns")
|
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) {
|
func TestService_ValidateAndSetDefaultsWithInvalidDNSQueryType(t *testing.T) {
|
||||||
|
@ -119,6 +121,8 @@ func TestService_ValidateAndSetDefaultsWithInvalidDNSQueryType(t *testing.T) {
|
||||||
QueryType: "B",
|
QueryType: "B",
|
||||||
QueryName: "example.com",
|
QueryName: "example.com",
|
||||||
}
|
}
|
||||||
dns.validateAndSetDefault()
|
err := 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...")
|
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...")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/client"
|
"github.com/TwinProduction/gatus/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ type Service struct {
|
||||||
Conditions []*Condition `yaml:"conditions"`
|
Conditions []*Condition `yaml:"conditions"`
|
||||||
|
|
||||||
// Alerts is the alerting configuration for the service in case of failure
|
// 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 is whether to skip verifying the server's certificate chain and host name
|
||||||
Insecure bool `yaml:"insecure,omitempty"`
|
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
|
// 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
|
// Set default values
|
||||||
if service.Interval == 0 {
|
if service.Interval == 0 {
|
||||||
service.Interval = 1 * time.Minute
|
service.Interval = 1 * time.Minute
|
||||||
|
@ -105,32 +106,32 @@ func (service *Service) ValidateAndSetDefaults() {
|
||||||
if _, contentTypeHeaderExists := service.Headers[ContentTypeHeader]; !contentTypeHeaderExists && service.GraphQL {
|
if _, contentTypeHeaderExists := service.Headers[ContentTypeHeader]; !contentTypeHeaderExists && service.GraphQL {
|
||||||
service.Headers[ContentTypeHeader] = "application/json"
|
service.Headers[ContentTypeHeader] = "application/json"
|
||||||
}
|
}
|
||||||
for _, alert := range service.Alerts {
|
for _, serviceAlert := range service.Alerts {
|
||||||
if alert.FailureThreshold <= 0 {
|
if serviceAlert.FailureThreshold <= 0 {
|
||||||
alert.FailureThreshold = 3
|
serviceAlert.FailureThreshold = 3
|
||||||
}
|
}
|
||||||
if alert.SuccessThreshold <= 0 {
|
if serviceAlert.SuccessThreshold <= 0 {
|
||||||
alert.SuccessThreshold = 2
|
serviceAlert.SuccessThreshold = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(service.Name) == 0 {
|
if len(service.Name) == 0 {
|
||||||
panic(ErrServiceWithNoName)
|
return ErrServiceWithNoName
|
||||||
}
|
}
|
||||||
if len(service.URL) == 0 {
|
if len(service.URL) == 0 {
|
||||||
panic(ErrServiceWithNoURL)
|
return ErrServiceWithNoURL
|
||||||
}
|
}
|
||||||
if len(service.Conditions) == 0 {
|
if len(service.Conditions) == 0 {
|
||||||
panic(ErrServiceWithNoCondition)
|
return ErrServiceWithNoCondition
|
||||||
}
|
}
|
||||||
if service.DNS != nil {
|
if service.DNS != nil {
|
||||||
service.DNS.validateAndSetDefault()
|
return service.DNS.validateAndSetDefault()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// Make sure that the request can be created
|
// Make sure that the request can be created
|
||||||
_, err := http.NewRequest(service.Method, service.URL, bytes.NewBuffer([]byte(service.Body)))
|
_, err := http.NewRequest(service.Method, service.URL, bytes.NewBuffer([]byte(service.Body)))
|
||||||
if err != nil {
|
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.
|
// EvaluateHealth sends a request to the service's URL and evaluates the conditions of the service.
|
||||||
|
@ -155,8 +156,8 @@ func (service *Service) EvaluateHealth() *Result {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAlertsTriggered returns a slice of alerts that have been triggered
|
// GetAlertsTriggered returns a slice of alerts that have been triggered
|
||||||
func (service *Service) GetAlertsTriggered() []Alert {
|
func (service *Service) GetAlertsTriggered() []alert.Alert {
|
||||||
var alerts []Alert
|
var alerts []alert.Alert
|
||||||
if service.NumberOfFailuresInARow == 0 {
|
if service.NumberOfFailuresInARow == 0 {
|
||||||
return alerts
|
return alerts
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestService_ValidateAndSetDefaults(t *testing.T) {
|
func TestService_ValidateAndSetDefaults(t *testing.T) {
|
||||||
|
@ -13,7 +15,7 @@ func TestService_ValidateAndSetDefaults(t *testing.T) {
|
||||||
Name: "twinnation-health",
|
Name: "twinnation-health",
|
||||||
URL: "https://twinnation.org/health",
|
URL: "https://twinnation.org/health",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []*Condition{&condition},
|
||||||
Alerts: []*Alert{{Type: PagerDutyAlert}},
|
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
|
||||||
}
|
}
|
||||||
service.ValidateAndSetDefaults()
|
service.ValidateAndSetDefaults()
|
||||||
if service.Method != "GET" {
|
if service.Method != "GET" {
|
||||||
|
@ -47,8 +49,10 @@ func TestService_ValidateAndSetDefaultsWithNoName(t *testing.T) {
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []*Condition{&condition},
|
||||||
}
|
}
|
||||||
service.ValidateAndSetDefaults()
|
err := service.ValidateAndSetDefaults()
|
||||||
t.Fatal("Should've panicked because service didn't have a name, which is a mandatory field")
|
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) {
|
func TestService_ValidateAndSetDefaultsWithNoUrl(t *testing.T) {
|
||||||
|
@ -59,8 +63,10 @@ func TestService_ValidateAndSetDefaultsWithNoUrl(t *testing.T) {
|
||||||
URL: "",
|
URL: "",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []*Condition{&condition},
|
||||||
}
|
}
|
||||||
service.ValidateAndSetDefaults()
|
err := service.ValidateAndSetDefaults()
|
||||||
t.Fatal("Should've panicked because service didn't have an url, which is a mandatory field")
|
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) {
|
func TestService_ValidateAndSetDefaultsWithNoConditions(t *testing.T) {
|
||||||
|
@ -70,8 +76,10 @@ func TestService_ValidateAndSetDefaultsWithNoConditions(t *testing.T) {
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Conditions: nil,
|
Conditions: nil,
|
||||||
}
|
}
|
||||||
service.ValidateAndSetDefaults()
|
err := service.ValidateAndSetDefaults()
|
||||||
t.Fatal("Should've panicked because service didn't have at least 1 condition")
|
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) {
|
func TestService_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||||
|
@ -85,7 +93,10 @@ func TestService_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||||
},
|
},
|
||||||
Conditions: []*Condition{&conditionSuccess},
|
Conditions: []*Condition{&conditionSuccess},
|
||||||
}
|
}
|
||||||
service.ValidateAndSetDefaults()
|
err := service.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
if service.DNS.QueryName != "example.com." {
|
if service.DNS.QueryName != "example.com." {
|
||||||
t.Error("Service.dns.query-name should be formatted with . suffix")
|
t.Error("Service.dns.query-name should be formatted with . suffix")
|
||||||
}
|
}
|
||||||
|
@ -98,7 +109,7 @@ func TestService_GetAlertsTriggered(t *testing.T) {
|
||||||
Name: "twinnation-health",
|
Name: "twinnation-health",
|
||||||
URL: "https://twinnation.org/health",
|
URL: "https://twinnation.org/health",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []*Condition{&condition},
|
||||||
Alerts: []*Alert{{Type: PagerDutyAlert, Enabled: &enabled}},
|
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty, Enabled: &enabled}},
|
||||||
}
|
}
|
||||||
service.ValidateAndSetDefaults()
|
service.ValidateAndSetDefaults()
|
||||||
if service.NumberOfFailuresInARow != 0 {
|
if service.NumberOfFailuresInARow != 0 {
|
||||||
|
|
77
main.go
77
main.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/config"
|
"github.com/TwinProduction/gatus/config"
|
||||||
"github.com/TwinProduction/gatus/controller"
|
"github.com/TwinProduction/gatus/controller"
|
||||||
|
@ -13,37 +14,75 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := loadConfiguration()
|
cfg, err := loadConfiguration()
|
||||||
go watchdog.Monitor(cfg)
|
if err != nil {
|
||||||
go controller.Handle()
|
panic(err)
|
||||||
|
}
|
||||||
|
start(cfg)
|
||||||
// Wait for termination signal
|
// Wait for termination signal
|
||||||
sig := make(chan os.Signal, 1)
|
signalChannel := make(chan os.Signal, 1)
|
||||||
done := make(chan bool, 1)
|
done := make(chan bool, 1)
|
||||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
<-sig
|
<-signalChannel
|
||||||
log.Println("Received termination signal, attempting to gracefully shut down")
|
log.Println("Received termination signal, attempting to gracefully shut down")
|
||||||
controller.Shutdown()
|
stop()
|
||||||
err := storage.Get().Save()
|
save()
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to save storage provider:", err.Error())
|
|
||||||
}
|
|
||||||
done <- true
|
done <- true
|
||||||
}()
|
}()
|
||||||
<-done
|
<-done
|
||||||
log.Println("Shutting down")
|
log.Println("Shutting down")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfiguration() *config.Config {
|
func stop() {
|
||||||
var err error
|
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")
|
customConfigFile := os.Getenv("GATUS_CONFIG_FILE")
|
||||||
if len(customConfigFile) > 0 {
|
if len(customConfigFile) > 0 {
|
||||||
err = config.Load(customConfigFile)
|
cfg, err = config.Load(customConfigFile)
|
||||||
} else {
|
} 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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/config"
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
@ -19,18 +18,16 @@ var (
|
||||||
// PublishMetricsForService publishes metrics for the given service and its result.
|
// PublishMetricsForService publishes metrics for the given service and its result.
|
||||||
// These metrics will be exposed at /metrics if the metrics are enabled
|
// These metrics will be exposed at /metrics if the metrics are enabled
|
||||||
func PublishMetricsForService(service *core.Service, result *core.Result) {
|
func PublishMetricsForService(service *core.Service, result *core.Result) {
|
||||||
if config.Get().Metrics {
|
rwLock.Lock()
|
||||||
rwLock.Lock()
|
gauge, exists := gauges[fmt.Sprintf("%s_%s", service.Name, service.URL)]
|
||||||
gauge, exists := gauges[fmt.Sprintf("%s_%s", service.Name, service.URL)]
|
if !exists {
|
||||||
if !exists {
|
gauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
gauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
Subsystem: "gatus",
|
||||||
Subsystem: "gatus",
|
Name: "tasks",
|
||||||
Name: "tasks",
|
ConstLabels: prometheus.Labels{"service": service.Name, "url": service.URL},
|
||||||
ConstLabels: prometheus.Labels{"service": service.Name, "url": service.URL},
|
}, []string{"status", "success"})
|
||||||
}, []string{"status", "success"})
|
gauges[fmt.Sprintf("%s_%s", service.Name, service.URL)] = gauge
|
||||||
gauges[fmt.Sprintf("%s_%s", service.Name, service.URL)] = gauge
|
|
||||||
}
|
|
||||||
rwLock.Unlock()
|
|
||||||
gauge.WithLabelValues(strconv.Itoa(result.HTTPStatus), strconv.FormatBool(result.Success)).Inc()
|
|
||||||
}
|
}
|
||||||
|
rwLock.Unlock()
|
||||||
|
gauge.WithLabelValues(strconv.Itoa(result.HTTPStatus), strconv.FormatBool(result.Success)).Inc()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -15,6 +16,9 @@ var (
|
||||||
// Because store.Store is an interface, a nil check wouldn't be sufficient, so instead of doing reflection
|
// 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
|
// every single time Get is called, we'll just lazily keep track of its existence through this variable
|
||||||
initialized bool
|
initialized bool
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get retrieves the storage provider
|
// Get retrieves the storage provider
|
||||||
|
@ -33,31 +37,38 @@ func Get() store.Store {
|
||||||
func Initialize(cfg *Config) error {
|
func Initialize(cfg *Config) error {
|
||||||
initialized = true
|
initialized = true
|
||||||
var err error
|
var err error
|
||||||
|
if cancelFunc != nil {
|
||||||
|
// Stop the active autoSave task
|
||||||
|
cancelFunc()
|
||||||
|
}
|
||||||
if cfg == nil || len(cfg.File) == 0 {
|
if cfg == nil || len(cfg.File) == 0 {
|
||||||
log.Println("[storage][Initialize] Creating storage provider")
|
log.Println("[storage][Initialize] Creating storage provider")
|
||||||
provider, err = memory.NewStore("")
|
provider, _ = memory.NewStore("")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
ctx, cancelFunc = context.WithCancel(context.Background())
|
||||||
log.Printf("[storage][Initialize] Creating storage provider with file=%s", cfg.File)
|
log.Printf("[storage][Initialize] Creating storage provider with file=%s", cfg.File)
|
||||||
provider, err = memory.NewStore(cfg.File)
|
provider, err = memory.NewStore(cfg.File)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go autoSave(7 * time.Minute)
|
go autoSave(7*time.Minute, ctx)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// autoSave automatically calls the Save function of the provider at every interval
|
// autoSave automatically calls the SaveFunc function of the provider at every interval
|
||||||
func autoSave(interval time.Duration) {
|
func autoSave(interval time.Duration, ctx context.Context) {
|
||||||
for {
|
for {
|
||||||
time.Sleep(interval)
|
select {
|
||||||
log.Printf("[storage][autoSave] Saving")
|
case <-ctx.Done():
|
||||||
err := provider.Save()
|
log.Printf("[storage][autoSave] Stopping active job")
|
||||||
if err != nil {
|
return
|
||||||
log.Println("[storage][autoSave] Save failed:", err.Error())
|
case <-time.After(interval):
|
||||||
|
log.Printf("[storage][autoSave] Saving")
|
||||||
|
err := provider.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[storage][autoSave] Save failed:", err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
37
storage/storage_test.go
Normal file
37
storage/storage_test.go
Normal file
|
@ -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
|
// Store is the interface that each stores should implement
|
||||||
type Store interface {
|
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
|
// with a subset of core.Result defined by the page and pageSize parameters
|
||||||
GetAllServiceStatusesWithResultPagination(page, pageSize int) map[string]*core.ServiceStatus
|
GetAllServiceStatusesWithResultPagination(page, pageSize int) map[string]*core.ServiceStatus
|
||||||
|
|
||||||
|
|
|
@ -4,96 +4,96 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/config"
|
"github.com/TwinProduction/gatus/alerting"
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleAlerting takes care of alerts to resolve and alerts to trigger based on result success or failure
|
// 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) {
|
func HandleAlerting(service *core.Service, result *core.Result, alertingConfig *alerting.Config, debug bool) {
|
||||||
cfg := config.Get()
|
if alertingConfig == nil {
|
||||||
if cfg.Alerting == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if result.Success {
|
if result.Success {
|
||||||
handleAlertsToResolve(service, result, cfg)
|
handleAlertsToResolve(service, result, alertingConfig, debug)
|
||||||
} else {
|
} 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.NumberOfSuccessesInARow = 0
|
||||||
service.NumberOfFailuresInARow++
|
service.NumberOfFailuresInARow++
|
||||||
for _, alert := range service.Alerts {
|
for _, serviceAlert := range service.Alerts {
|
||||||
// If the alert hasn't been triggered, move to the next one
|
// If the serviceAlert hasn't been triggered, move to the next one
|
||||||
if !alert.IsEnabled() || alert.FailureThreshold > service.NumberOfFailuresInARow {
|
if !serviceAlert.IsEnabled() || serviceAlert.FailureThreshold > service.NumberOfFailuresInARow {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if alert.Triggered {
|
if serviceAlert.Triggered {
|
||||||
if cfg.Debug {
|
if debug {
|
||||||
log.Printf("[watchdog][handleAlertsToTrigger] Alert for service=%s with description='%s' has already been TRIGGERED, skipping", service.Name, alert.GetDescription())
|
log.Printf("[watchdog][handleAlertsToTrigger] Alert for service=%s with description='%s' has already been TRIGGERED, skipping", service.Name, serviceAlert.GetDescription())
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
alertProvider := config.GetAlertingProviderByAlertType(cfg, alert.Type)
|
alertProvider := alertingConfig.GetAlertingProviderByAlertType(serviceAlert.Type)
|
||||||
if alertProvider != nil && alertProvider.IsValid() {
|
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.GetDescription())
|
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, alert, result, false)
|
customAlertProvider := alertProvider.ToCustomAlertProvider(service, serviceAlert, result, false)
|
||||||
// TODO: retry on error
|
// TODO: retry on error
|
||||||
var err error
|
var err error
|
||||||
// We need to extract the DedupKey from PagerDuty's response
|
// We need to extract the DedupKey from PagerDuty's response
|
||||||
if alert.Type == core.PagerDutyAlert {
|
if serviceAlert.Type == alert.TypePagerDuty {
|
||||||
var body []byte
|
var body []byte
|
||||||
if body, err = customAlertProvider.Send(service.Name, alert.GetDescription(), false); err == nil {
|
if body, err = customAlertProvider.Send(service.Name, serviceAlert.GetDescription(), false); err == nil {
|
||||||
var response pagerDutyResponse
|
var response pagerDutyResponse
|
||||||
if err = json.Unmarshal(body, &response); err != nil {
|
if err = json.Unmarshal(body, &response); err != nil {
|
||||||
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error unmarshaling pagerduty response: %s", err.Error())
|
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error unmarshaling pagerduty response: %s", err.Error())
|
||||||
} else {
|
} else {
|
||||||
alert.ResolveKey = response.DedupKey
|
serviceAlert.ResolveKey = response.DedupKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// All other alert types don't need to extract anything from the body, so we can just send the request right away
|
// 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, alert.GetDescription(), false)
|
_, err = customAlertProvider.Send(service.Name, serviceAlert.GetDescription(), false)
|
||||||
}
|
}
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
alert.Triggered = true
|
serviceAlert.Triggered = true
|
||||||
}
|
}
|
||||||
} else {
|
} 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++
|
service.NumberOfSuccessesInARow++
|
||||||
for _, alert := range service.Alerts {
|
for _, serviceAlert := range service.Alerts {
|
||||||
if !alert.IsEnabled() || !alert.Triggered || alert.SuccessThreshold > service.NumberOfSuccessesInARow {
|
if !serviceAlert.IsEnabled() || !serviceAlert.Triggered || serviceAlert.SuccessThreshold > service.NumberOfSuccessesInARow {
|
||||||
continue
|
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.
|
// Further explanation can be found on Alert's Triggered field.
|
||||||
alert.Triggered = false
|
serviceAlert.Triggered = false
|
||||||
if !alert.IsSendingOnResolved() {
|
if !serviceAlert.IsSendingOnResolved() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
alertProvider := config.GetAlertingProviderByAlertType(cfg, alert.Type)
|
alertProvider := alertingConfig.GetAlertingProviderByAlertType(serviceAlert.Type)
|
||||||
if alertProvider != nil && alertProvider.IsValid() {
|
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.GetDescription())
|
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, alert, result, true)
|
customAlertProvider := alertProvider.ToCustomAlertProvider(service, serviceAlert, result, true)
|
||||||
// TODO: retry on error
|
// TODO: retry on error
|
||||||
_, err := customAlertProvider.Send(service.Name, alert.GetDescription(), true)
|
_, err := customAlertProvider.Send(service.Name, serviceAlert.GetDescription(), true)
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
if alert.Type == core.PagerDutyAlert {
|
if serviceAlert.Type == alert.TypePagerDuty {
|
||||||
alert.ResolveKey = ""
|
serviceAlert.ResolveKey = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
service.NumberOfFailuresInARow = 0
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/alerting"
|
"github.com/TwinProduction/gatus/alerting"
|
||||||
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/pagerduty"
|
"github.com/TwinProduction/gatus/alerting/provider/pagerduty"
|
||||||
"github.com/TwinProduction/gatus/config"
|
"github.com/TwinProduction/gatus/config"
|
||||||
|
@ -24,13 +25,12 @@ func TestHandleAlerting(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config.Set(cfg)
|
|
||||||
enabled := true
|
enabled := true
|
||||||
service := &core.Service{
|
service := &core.Service{
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Alerts: []*core.Alert{
|
Alerts: []*alert.Alert{
|
||||||
{
|
{
|
||||||
Type: core.CustomAlert,
|
Type: alert.TypeCustom,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
FailureThreshold: 2,
|
FailureThreshold: 2,
|
||||||
SuccessThreshold: 3,
|
SuccessThreshold: 3,
|
||||||
|
@ -41,50 +41,40 @@ func TestHandleAlerting(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(t, service, 0, 0, false, "The alert shouldn't start triggered")
|
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")
|
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")
|
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")
|
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")
|
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)")
|
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)")
|
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")
|
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")
|
verify(t, service, 0, 4, false, "The alert should no longer be triggered")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleAlertingWhenAlertingConfigIsNil(t *testing.T) {
|
func TestHandleAlertingWhenAlertingConfigIsNil(t *testing.T) {
|
||||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||||
defer os.Clearenv()
|
defer os.Clearenv()
|
||||||
|
HandleAlerting(nil, nil, nil, true)
|
||||||
cfg := &config.Config{
|
|
||||||
Debug: true,
|
|
||||||
Alerting: nil,
|
|
||||||
}
|
|
||||||
config.Set(cfg)
|
|
||||||
HandleAlerting(nil, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleAlertingWithBadAlertProvider(t *testing.T) {
|
func TestHandleAlertingWithBadAlertProvider(t *testing.T) {
|
||||||
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
_ = os.Setenv("MOCK_ALERT_PROVIDER", "true")
|
||||||
defer os.Clearenv()
|
defer os.Clearenv()
|
||||||
|
|
||||||
cfg := &config.Config{
|
|
||||||
Alerting: &alerting.Config{},
|
|
||||||
}
|
|
||||||
config.Set(cfg)
|
|
||||||
enabled := true
|
enabled := true
|
||||||
service := &core.Service{
|
service := &core.Service{
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Alerts: []*core.Alert{
|
Alerts: []*alert.Alert{
|
||||||
{
|
{
|
||||||
Type: core.CustomAlert,
|
Type: alert.TypeCustom,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
FailureThreshold: 1,
|
FailureThreshold: 1,
|
||||||
SuccessThreshold: 1,
|
SuccessThreshold: 1,
|
||||||
|
@ -95,9 +85,9 @@ func TestHandleAlertingWithBadAlertProvider(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(t, service, 0, 0, false, "The alert shouldn't start triggered")
|
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")
|
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")
|
verify(t, service, 2, 0, false, "The alert shouldn't have triggered, because the provider wasn't configured properly")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,13 +104,12 @@ func TestHandleAlertingWhenTriggeredAlertIsAlmostResolvedButServiceStartFailingA
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config.Set(cfg)
|
|
||||||
enabled := true
|
enabled := true
|
||||||
service := &core.Service{
|
service := &core.Service{
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Alerts: []*core.Alert{
|
Alerts: []*alert.Alert{
|
||||||
{
|
{
|
||||||
Type: core.CustomAlert,
|
Type: alert.TypeCustom,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
FailureThreshold: 2,
|
FailureThreshold: 2,
|
||||||
SuccessThreshold: 3,
|
SuccessThreshold: 3,
|
||||||
|
@ -132,7 +121,7 @@ func TestHandleAlertingWhenTriggeredAlertIsAlmostResolvedButServiceStartFailingA
|
||||||
}
|
}
|
||||||
|
|
||||||
// This test simulate an alert that was already triggered
|
// 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")
|
verify(t, service, 2, 0, true, "The alert was already triggered at the beginning of this test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,14 +138,13 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedButSendOnResolvedIsFalse(t *t
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config.Set(cfg)
|
|
||||||
enabled := true
|
enabled := true
|
||||||
disabled := false
|
disabled := false
|
||||||
service := &core.Service{
|
service := &core.Service{
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Alerts: []*core.Alert{
|
Alerts: []*alert.Alert{
|
||||||
{
|
{
|
||||||
Type: core.CustomAlert,
|
Type: alert.TypeCustom,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
FailureThreshold: 1,
|
FailureThreshold: 1,
|
||||||
SuccessThreshold: 1,
|
SuccessThreshold: 1,
|
||||||
|
@ -167,7 +155,7 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedButSendOnResolvedIsFalse(t *t
|
||||||
NumberOfFailuresInARow: 1,
|
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")
|
verify(t, service, 0, 1, false, "The alert should've been resolved")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,13 +171,12 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedPagerDuty(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config.Set(cfg)
|
|
||||||
enabled := true
|
enabled := true
|
||||||
service := &core.Service{
|
service := &core.Service{
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Alerts: []*core.Alert{
|
Alerts: []*alert.Alert{
|
||||||
{
|
{
|
||||||
Type: core.PagerDutyAlert,
|
Type: alert.TypePagerDuty,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
FailureThreshold: 1,
|
FailureThreshold: 1,
|
||||||
SuccessThreshold: 1,
|
SuccessThreshold: 1,
|
||||||
|
@ -200,10 +187,10 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedPagerDuty(t *testing.T) {
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleAlerting(service, &core.Result{Success: false})
|
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||||
verify(t, service, 1, 0, true, "")
|
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")
|
verify(t, service, 0, 1, false, "The alert should've been resolved")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,13 +207,12 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config.Set(cfg)
|
|
||||||
enabled := true
|
enabled := true
|
||||||
service := &core.Service{
|
service := &core.Service{
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Alerts: []*core.Alert{
|
Alerts: []*alert.Alert{
|
||||||
{
|
{
|
||||||
Type: core.CustomAlert,
|
Type: alert.TypeCustom,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
FailureThreshold: 2,
|
FailureThreshold: 2,
|
||||||
SuccessThreshold: 2,
|
SuccessThreshold: 2,
|
||||||
|
@ -237,32 +223,32 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "true")
|
_ = 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, "")
|
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")
|
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")
|
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")
|
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")
|
_ = 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")
|
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")
|
verify(t, service, 0, 1, true, "The alert should've still been triggered")
|
||||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "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, 2, false, "The alert should've been resolved DESPITE THE ALERT PROVIDER RETURNING AN ERROR. See Alert.Triggered for further explanation.")
|
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")
|
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||||
|
|
||||||
// Make sure that everything's working as expected after a rough patch
|
// 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, "")
|
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")
|
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")
|
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")
|
verify(t, service, 0, 2, false, "The alert should have been resolved")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,13 +265,12 @@ func TestHandleAlertingWithProviderThatOnlyReturnsErrorOnResolve(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config.Set(cfg)
|
|
||||||
enabled := true
|
enabled := true
|
||||||
service := &core.Service{
|
service := &core.Service{
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Alerts: []*core.Alert{
|
Alerts: []*alert.Alert{
|
||||||
{
|
{
|
||||||
Type: core.CustomAlert,
|
Type: alert.TypeCustom,
|
||||||
Enabled: &enabled,
|
Enabled: &enabled,
|
||||||
FailureThreshold: 1,
|
FailureThreshold: 1,
|
||||||
SuccessThreshold: 1,
|
SuccessThreshold: 1,
|
||||||
|
@ -295,27 +280,27 @@ func TestHandleAlertingWithProviderThatOnlyReturnsErrorOnResolve(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleAlerting(service, &core.Result{Success: false})
|
HandleAlerting(service, &core.Result{Success: false}, cfg.Alerting, cfg.Debug)
|
||||||
verify(t, service, 1, 0, true, "")
|
verify(t, service, 1, 0, true, "")
|
||||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "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, "")
|
verify(t, service, 0, 1, false, "")
|
||||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "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, "")
|
verify(t, service, 1, 0, true, "")
|
||||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "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, "")
|
verify(t, service, 0, 1, false, "")
|
||||||
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
_ = os.Setenv("MOCK_ALERT_PROVIDER_ERROR", "false")
|
||||||
|
|
||||||
// Make sure that everything's working as expected after a rough patch
|
// 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, "")
|
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, "")
|
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, "")
|
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, "")
|
verify(t, service, 0, 2, false, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package watchdog
|
package watchdog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/alerting"
|
||||||
"github.com/TwinProduction/gatus/config"
|
"github.com/TwinProduction/gatus/config"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
"github.com/TwinProduction/gatus/metric"
|
"github.com/TwinProduction/gatus/metric"
|
||||||
|
@ -15,48 +17,65 @@ var (
|
||||||
// monitoringMutex is used to prevent multiple services from being evaluated at the same time.
|
// monitoringMutex is used to prevent multiple services from being evaluated at the same time.
|
||||||
// Without this, conditions using response time may become inaccurate.
|
// Without this, conditions using response time may become inaccurate.
|
||||||
monitoringMutex sync.Mutex
|
monitoringMutex sync.Mutex
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
)
|
)
|
||||||
|
|
||||||
// Monitor loops over each services and starts a goroutine to monitor each services separately
|
// Monitor loops over each services and starts a goroutine to monitor each services separately
|
||||||
func Monitor(cfg *config.Config) {
|
func Monitor(cfg *config.Config) {
|
||||||
|
ctx, cancelFunc = context.WithCancel(context.Background())
|
||||||
for _, service := range cfg.Services {
|
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
|
// 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)
|
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
|
// monitor monitors a single service in a loop
|
||||||
func monitor(service *core.Service) {
|
func monitor(service *core.Service, alertingConfig *alerting.Config, disableMonitoringLock, enabledMetrics, debug bool, ctx context.Context) {
|
||||||
cfg := config.Get()
|
// Run it immediately on start
|
||||||
|
execute(service, alertingConfig, disableMonitoringLock, enabledMetrics, debug)
|
||||||
|
// Loop for the next executions
|
||||||
for {
|
for {
|
||||||
if !cfg.DisableMonitoringLock {
|
select {
|
||||||
// By placing the lock here, we prevent multiple services from being monitored at the exact same time, which
|
case <-ctx.Done():
|
||||||
// could cause performance issues and return inaccurate results
|
log.Printf("[watchdog][monitor] Canceling current execution of group=%s; service=%s", service.Group, service.Name)
|
||||||
monitoringMutex.Lock()
|
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)
|
metric.PublishMetricsForService(service, result)
|
||||||
UpdateServiceStatuses(service, result)
|
}
|
||||||
log.Printf(
|
UpdateServiceStatuses(service, result)
|
||||||
"[watchdog][monitor] Monitored group=%s; service=%s; success=%v; errors=%d; duration=%s",
|
log.Printf(
|
||||||
service.Group,
|
"[watchdog][execute] Monitored group=%s; service=%s; success=%v; errors=%d; duration=%s",
|
||||||
service.Name,
|
service.Group,
|
||||||
result.Success,
|
service.Name,
|
||||||
len(result.Errors),
|
result.Success,
|
||||||
result.Duration.Round(time.Millisecond),
|
len(result.Errors),
|
||||||
)
|
result.Duration.Round(time.Millisecond),
|
||||||
HandleAlerting(service, result)
|
)
|
||||||
if cfg.Debug {
|
HandleAlerting(service, result, alertingConfig, debug)
|
||||||
log.Printf("[watchdog][monitor] Waiting for interval=%s before monitoring group=%s service=%s again", service.Interval, service.Group, service.Name)
|
if debug {
|
||||||
}
|
log.Printf("[watchdog][execute] Waiting for interval=%s before monitoring group=%s service=%s again", service.Interval, service.Group, service.Name)
|
||||||
if !cfg.DisableMonitoringLock {
|
}
|
||||||
monitoringMutex.Unlock()
|
if !disableMonitoringLock {
|
||||||
}
|
monitoringMutex.Unlock()
|
||||||
time.Sleep(service.Interval)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,3 +83,8 @@ func monitor(service *core.Service) {
|
||||||
func UpdateServiceStatuses(service *core.Service, result *core.Result) {
|
func UpdateServiceStatuses(service *core.Service, result *core.Result) {
|
||||||
storage.Get().Insert(service, result)
|
storage.Get().Insert(service, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown stops monitoring all services
|
||||||
|
func Shutdown() {
|
||||||
|
cancelFunc()
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
|
<title>Health Dashboard | Gatus</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
<title>Health Dashboard | Gatus</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="dark:bg-gray-900">
|
<body class="dark:bg-gray-900">
|
||||||
<noscript>
|
<noscript><strong>Enable JavaScript to view this page.</strong></noscript>
|
||||||
<strong>Enable JavaScript to view this page.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue