mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-14 11:58:04 +00:00
refactor: Break core package into multiple packages under config/endpoint (#759)
* refactor: Partially break core package into dns, result and ssh packages * refactor: Move core package to config/endpoint * refactor: Fix warning about overlapping imported package name with endpoint variable * refactor: Rename EndpointStatus to Status * refactor: Merge result pkg back into endpoint pkg, because it makes more sense * refactor: Rename parameter r to result in Condition.evaluate * refactor: Rename parameter r to result * refactor: Revert accidental change to endpoint.TypeDNS * refactor: Rename parameter r to result * refactor: Merge util package into endpoint package * refactor: Rename parameter r to result
This commit is contained in:
parent
4397dcb5fc
commit
9d151fcdb4
104 changed files with 1216 additions and 1211 deletions
|
@ -10,7 +10,7 @@ var (
|
||||||
ErrAlertWithInvalidDescription = errors.New("alert description must not have \" or \\")
|
ErrAlertWithInvalidDescription = errors.New("alert description must not have \" or \\")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Alert is a core.Endpoint's alert configuration
|
// Alert is a endpoint.Endpoint's alert configuration
|
||||||
type Alert struct {
|
type Alert struct {
|
||||||
// Type of alert (required)
|
// Type of alert (required)
|
||||||
Type Type `yaml:"type"`
|
Type Type `yaml:"type"`
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
@ -57,14 +57,14 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
sess, err := provider.createSession()
|
sess, err := provider.createSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
svc := ses.New(sess)
|
svc := ses.New(sess)
|
||||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
|
||||||
emails := strings.Split(provider.getToForGroup(endpoint.Group), ",")
|
emails := strings.Split(provider.getToForGroup(ep.Group), ",")
|
||||||
|
|
||||||
input := &ses.SendEmailInput{
|
input := &ses.SendEmailInput{
|
||||||
Destination: &ses.Destination{
|
Destination: &ses.Destination{
|
||||||
|
@ -110,14 +110,14 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildMessageSubjectAndBody builds the message subject and body
|
// buildMessageSubjectAndBody builds the message subject and body
|
||||||
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
|
func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
|
||||||
var subject, message string
|
var subject, message string
|
||||||
if resolved {
|
if resolved {
|
||||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert resolved", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert triggered", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
|
@ -95,10 +95,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
|
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
|
||||||
|
@ -50,16 +50,16 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *alert.Alert, resolved bool) *http.Request {
|
func (provider *AlertProvider) buildHTTPRequest(ep *endpoint.Endpoint, alert *alert.Alert, resolved bool) *http.Request {
|
||||||
body, url, method := provider.Body, provider.URL, provider.Method
|
body, url, method := provider.Body, provider.URL, provider.Method
|
||||||
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||||
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpoint.Name)
|
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", ep.Name)
|
||||||
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", endpoint.Name)
|
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", ep.Name)
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", endpoint.Group)
|
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", ep.Group)
|
||||||
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", endpoint.Group)
|
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", ep.Group)
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", endpoint.URL)
|
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", ep.URL)
|
||||||
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", endpoint.URL)
|
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", ep.URL)
|
||||||
if resolved {
|
if resolved {
|
||||||
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||||
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||||
|
@ -78,8 +78,8 @@ func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
request := provider.buildHTTPRequest(endpoint, alert, resolved)
|
request := provider.buildHTTPRequest(ep, alert, resolved)
|
||||||
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -90,10 +90,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -138,7 +138,7 @@ func TestAlertProvider_buildHTTPRequest(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
|
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||||
request := customAlertProvider.buildHTTPRequest(
|
request := customAlertProvider.buildHTTPRequest(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
|
||||||
&alert.Alert{Description: &alertDescription},
|
&alert.Alert{Description: &alertDescription},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
|
@ -188,7 +188,7 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
|
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||||
request := customAlertProvider.buildHTTPRequest(
|
request := customAlertProvider.buildHTTPRequest(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&alert.Alert{Description: &alertDescription},
|
&alert.Alert{Description: &alertDescription},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Discord
|
// AlertProvider is the configuration necessary for sending an alert using Discord
|
||||||
|
@ -47,9 +47,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -85,14 +85,14 @@ type Field struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
var colorCode int
|
var colorCode int
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
colorCode = 3066993
|
colorCode = 3066993
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
colorCode = 15158332
|
colorCode = 15158332
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -127,10 +127,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -191,18 +191,18 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
{Condition: "[BODY] != \"\"", Success: scenario.Resolved},
|
{Condition: "[BODY] != \"\"", Success: scenario.Resolved},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: conditionResults,
|
ConditionResults: conditionResults,
|
||||||
},
|
},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
gomail "gopkg.in/mail.v2"
|
gomail "gopkg.in/mail.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,17 +53,17 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
var username string
|
var username string
|
||||||
if len(provider.Username) > 0 {
|
if len(provider.Username) > 0 {
|
||||||
username = provider.Username
|
username = provider.Username
|
||||||
} else {
|
} else {
|
||||||
username = provider.From
|
username = provider.From
|
||||||
}
|
}
|
||||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
|
||||||
m := gomail.NewMessage()
|
m := gomail.NewMessage()
|
||||||
m.SetHeader("From", provider.From)
|
m.SetHeader("From", provider.From)
|
||||||
m.SetHeader("To", strings.Split(provider.getToForGroup(endpoint.Group), ",")...)
|
m.SetHeader("To", strings.Split(provider.getToForGroup(ep.Group), ",")...)
|
||||||
m.SetHeader("Subject", subject)
|
m.SetHeader("Subject", subject)
|
||||||
m.SetBody("text/plain", body)
|
m.SetBody("text/plain", body)
|
||||||
var d *gomail.Dialer
|
var d *gomail.Dialer
|
||||||
|
@ -87,14 +87,14 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildMessageSubjectAndBody builds the message subject and body
|
// buildMessageSubjectAndBody builds the message subject and body
|
||||||
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
|
func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
|
||||||
var subject, message string
|
var subject, message string
|
||||||
if resolved {
|
if resolved {
|
||||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert resolved", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert triggered", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
|
@ -97,10 +97,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/google/go-github/v48/github"
|
"github.com/google/go-github/v48/github"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
@ -70,12 +70,12 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
|
|
||||||
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
||||||
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
title := "alert(gatus): " + endpoint.DisplayName()
|
title := "alert(gatus): " + ep.DisplayName()
|
||||||
if !resolved {
|
if !resolved {
|
||||||
_, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
|
_, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
|
||||||
Title: github.String(title),
|
Title: github.String(title),
|
||||||
Body: github.String(provider.buildIssueBody(endpoint, alert, result)),
|
Body: github.String(provider.buildIssueBody(ep, alert, result)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create issue: %w", err)
|
return fmt.Errorf("failed to create issue: %w", err)
|
||||||
|
@ -104,7 +104,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildIssueBody builds the body of the issue
|
// buildIssueBody builds the body of the issue
|
||||||
func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result) string {
|
func (provider *AlertProvider) buildIssueBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result) string {
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
formattedConditionResults = "\n\n## Condition results\n"
|
formattedConditionResults = "\n\n## Condition results\n"
|
||||||
|
@ -122,7 +122,7 @@ func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *al
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = ":\n> " + alertDescription
|
description = ":\n> " + alertDescription
|
||||||
}
|
}
|
||||||
message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
return message + description + formattedConditionResults
|
return message + description + formattedConditionResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
"github.com/google/go-github/v48/github"
|
"github.com/google/go-github/v48/github"
|
||||||
)
|
)
|
||||||
|
@ -85,10 +85,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
scenario.Provider.githubClient = github.NewClient(nil)
|
scenario.Provider.githubClient = github.NewClient(nil)
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -109,7 +109,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
NoConditions bool
|
NoConditions bool
|
||||||
|
@ -117,14 +117,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
||||||
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "triggered-with-no-description",
|
Name: "triggered-with-no-description",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{FailureThreshold: 10},
|
Alert: alert.Alert{FailureThreshold: 10},
|
||||||
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
||||||
|
@ -132,7 +132,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "triggered-with-no-conditions",
|
Name: "triggered-with-no-conditions",
|
||||||
NoConditions: true,
|
NoConditions: true,
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 10},
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 10},
|
||||||
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row:\n> description-1",
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row:\n> description-1",
|
||||||
|
@ -140,9 +140,9 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: true},
|
{Condition: "[CONNECTED] == true", Success: true},
|
||||||
{Condition: "[STATUS] == 200", Success: false},
|
{Condition: "[STATUS] == 200", Success: false},
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
body := scenario.Provider.buildIssueBody(
|
body := scenario.Provider.buildIssueBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{ConditionResults: conditionResults},
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
)
|
)
|
||||||
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
|
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
|
||||||
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,11 +51,11 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
|
|
||||||
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
||||||
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
if len(alert.ResolveKey) == 0 {
|
if len(alert.ResolveKey) == 0 {
|
||||||
alert.ResolveKey = uuid.NewString()
|
alert.ResolveKey = uuid.NewString()
|
||||||
}
|
}
|
||||||
buffer := bytes.NewBuffer(provider.buildAlertBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildAlertBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -94,21 +94,21 @@ func (provider *AlertProvider) monitoringTool() string {
|
||||||
return "gatus"
|
return "gatus"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) service(endpoint *core.Endpoint) string {
|
func (provider *AlertProvider) service(ep *endpoint.Endpoint) string {
|
||||||
if len(provider.Service) > 0 {
|
if len(provider.Service) > 0 {
|
||||||
return provider.Service
|
return provider.Service
|
||||||
}
|
}
|
||||||
return endpoint.DisplayName()
|
return ep.DisplayName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildAlertBody builds the body of the alert
|
// buildAlertBody builds the body of the alert
|
||||||
func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildAlertBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
body := AlertBody{
|
body := AlertBody{
|
||||||
Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(endpoint)),
|
Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(ep)),
|
||||||
StartTime: result.Timestamp.Format(time.RFC3339),
|
StartTime: result.Timestamp.Format(time.RFC3339),
|
||||||
Service: provider.service(endpoint),
|
Service: provider.service(ep),
|
||||||
MonitoringTool: provider.monitoringTool(),
|
MonitoringTool: provider.monitoringTool(),
|
||||||
Hosts: endpoint.URL,
|
Hosts: ep.URL,
|
||||||
GitlabEnvironmentName: provider.EnvironmentName,
|
GitlabEnvironmentName: provider.EnvironmentName,
|
||||||
Severity: provider.Severity,
|
Severity: provider.Severity,
|
||||||
Fingerprint: alert.ResolveKey,
|
Fingerprint: alert.ResolveKey,
|
||||||
|
@ -135,9 +135,9 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al
|
||||||
}
|
}
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
body.Description = message + description + formattedConditionResults
|
body.Description = message + description + formattedConditionResults
|
||||||
bodyAsJSON, _ := json.Marshal(body)
|
bodyAsJSON, _ := json.Marshal(body)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -84,10 +84,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -108,21 +108,21 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
|
||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
||||||
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "no-description",
|
Name: "no-description",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{FailureThreshold: 10},
|
Alert: alert.Alert{FailureThreshold: 10},
|
||||||
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 10 time(s) in a row\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 10 time(s) in a row\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
||||||
|
@ -133,8 +133,8 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
|
||||||
body := scenario.Provider.buildAlertBody(
|
body := scenario.Provider.buildAlertBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: true},
|
{Condition: "[CONNECTED] == true", Success: true},
|
||||||
{Condition: "[STATUS] == 200", Success: false},
|
{Condition: "[STATUS] == 200", Success: false},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Google chat
|
// AlertProvider is the configuration necessary for sending an alert using Google chat
|
||||||
|
@ -50,9 +50,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ type OpenLink struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
if resolved {
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
|
@ -143,7 +143,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||||
Widgets: []Widgets{
|
Widgets: []Widgets{
|
||||||
{
|
{
|
||||||
KeyValue: &KeyValue{
|
KeyValue: &KeyValue{
|
||||||
TopLabel: endpoint.DisplayName(),
|
TopLabel: ep.DisplayName(),
|
||||||
Content: message,
|
Content: message,
|
||||||
ContentMultiline: "true",
|
ContentMultiline: "true",
|
||||||
BottomLabel: description,
|
BottomLabel: description,
|
||||||
|
@ -166,7 +166,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if endpoint.Type() == core.EndpointTypeHTTP {
|
if ep.Type() == endpoint.TypeHTTP {
|
||||||
// We only include a button targeting the URL if the endpoint is an HTTP endpoint
|
// We only include a button targeting the URL if the endpoint is an HTTP endpoint
|
||||||
// If the URL isn't prefixed with https://, Google Chat will just display a blank message aynways.
|
// If the URL isn't prefixed with https://, Google Chat will just display a blank message aynways.
|
||||||
// See https://github.com/TwiN/gatus/issues/362
|
// See https://github.com/TwiN/gatus/issues/362
|
||||||
|
@ -175,7 +175,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||||
{
|
{
|
||||||
TextButton: TextButton{
|
TextButton: TextButton{
|
||||||
Text: "URL",
|
Text: "URL",
|
||||||
OnClick: OnClick{OpenLink: OpenLink{URL: endpoint.URL}},
|
OnClick: OnClick{OpenLink: OpenLink{URL: ep.URL}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -141,7 +141,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
secondDescription := "description-2"
|
secondDescription := "description-2"
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
Resolved bool
|
Resolved bool
|
||||||
|
@ -149,7 +149,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
|
@ -157,7 +157,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
|
@ -165,7 +165,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "icmp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
Name: "icmp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
|
@ -173,7 +173,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "tcp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
Name: "tcp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
|
@ -185,8 +185,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultPriority = 5
|
const DefaultPriority = 5
|
||||||
|
@ -41,8 +41,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -67,12 +67,12 @@ type Body struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
@ -88,7 +88,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||||
message += " with the following description: " + alert.GetDescription()
|
message += " with the following description: " + alert.GetDescription()
|
||||||
}
|
}
|
||||||
message += formattedConditionResults
|
message += formattedConditionResults
|
||||||
title := "Gatus: " + endpoint.DisplayName()
|
title := "Gatus: " + ep.DisplayName()
|
||||||
if provider.Title != "" {
|
if provider.Title != "" {
|
||||||
title = provider.Title
|
title = provider.Title
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
@ -49,7 +49,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
description = "custom-description"
|
description = "custom-description"
|
||||||
//title = "custom-title"
|
//title = "custom-title"
|
||||||
endpoint = "custom-endpoint"
|
endpointName = "custom-endpoint"
|
||||||
)
|
)
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -63,30 +63,30 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
|
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpointName, description),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been resolved after passing successfully 5 time(s) in a row with the following description: %s\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
|
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been resolved after passing successfully 5 time(s) in a row with the following description: %s\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpointName, description),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "custom-title",
|
Name: "custom-title",
|
||||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
|
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
|
||||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"custom-title\",\"priority\":0}", endpoint, description),
|
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"custom-title\",\"priority\":0}", endpointName, description),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: endpoint},
|
&endpoint.Endpoint{Name: endpointName},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using JetBrains Space
|
// AlertProvider is the configuration necessary for sending an alert using JetBrains Space
|
||||||
|
@ -46,8 +46,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project)
|
url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project)
|
||||||
request, err := http.NewRequest(http.MethodPost, url, buffer)
|
request, err := http.NewRequest(http.MethodPost, url, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -103,9 +103,9 @@ type Icon struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
body := Body{
|
body := Body{
|
||||||
Channel: "id:" + provider.getChannelIDForGroup(endpoint.Group),
|
Channel: "id:" + provider.getChannelIDForGroup(ep.Group),
|
||||||
Content: Content{
|
Content: Content{
|
||||||
ClassName: "ChatMessage.Block",
|
ClassName: "ChatMessage.Block",
|
||||||
Sections: []Section{{
|
Sections: []Section{{
|
||||||
|
@ -116,10 +116,10 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||||
}
|
}
|
||||||
if resolved {
|
if resolved {
|
||||||
body.Content.Style = "SUCCESS"
|
body.Content.Style = "SUCCESS"
|
||||||
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
body.Content.Style = "WARNING"
|
body.Content.Style = "WARNING"
|
||||||
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
icon := "warning"
|
icon := "warning"
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -120,10 +120,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -146,7 +146,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
Resolved bool
|
Resolved bool
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
|
@ -154,7 +154,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
||||||
|
@ -162,7 +162,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "triggered-with-group",
|
Name: "triggered-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
||||||
|
@ -170,7 +170,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
||||||
|
@ -178,7 +178,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "resolved-with-group",
|
Name: "resolved-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
||||||
|
@ -189,8 +189,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Matrix
|
// AlertProvider is the configuration necessary for sending an alert using Matrix
|
||||||
|
@ -61,9 +61,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
config := provider.getConfigForGroup(endpoint.Group)
|
config := provider.getConfigForGroup(ep.Group)
|
||||||
if config.ServerURL == "" {
|
if config.ServerURL == "" {
|
||||||
config.ServerURL = defaultServerURL
|
config.ServerURL = defaultServerURL
|
||||||
}
|
}
|
||||||
|
@ -103,23 +103,23 @@ type Body struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
MsgType: "m.text",
|
MsgType: "m.text",
|
||||||
Format: "org.matrix.custom.html",
|
Format: "org.matrix.custom.html",
|
||||||
Body: buildPlaintextMessageBody(endpoint, alert, result, resolved),
|
Body: buildPlaintextMessageBody(ep, alert, result, resolved),
|
||||||
FormattedBody: buildHTMLMessageBody(endpoint, alert, result, resolved),
|
FormattedBody: buildHTMLMessageBody(ep, alert, result, resolved),
|
||||||
})
|
})
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildPlaintextMessageBody builds the message body in plaintext to include in request
|
// buildPlaintextMessageBody builds the message body in plaintext to include in request
|
||||||
func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func buildPlaintextMessageBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
@ -139,12 +139,12 @@ func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, resu
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildHTMLMessageBody builds the message body in HTML to include in request
|
// buildHTMLMessageBody builds the message body in HTML to include in request
|
||||||
func buildHTMLMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func buildHTMLMessageBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -149,10 +149,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -197,10 +197,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
||||||
|
@ -50,9 +50,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -92,13 +92,13 @@ type Field struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -120,10 +120,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -168,10 +168,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -33,8 +33,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
// 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) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -60,12 +60,12 @@ type Body struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
Originator: provider.Originator,
|
Originator: provider.Originator,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -83,10 +83,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -131,10 +131,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -46,8 +46,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -77,7 +77,7 @@ type Body struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, formattedConditionResults, tag string
|
var message, formattedConditionResults, tag string
|
||||||
if resolved {
|
if resolved {
|
||||||
tag = "white_check_mark"
|
tag = "white_check_mark"
|
||||||
|
@ -101,7 +101,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||||
message += formattedConditionResults
|
message += formattedConditionResults
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
Topic: provider.Topic,
|
Topic: provider.Topic,
|
||||||
Title: "Gatus: " + endpoint.DisplayName(),
|
Title: "Gatus: " + ep.DisplayName(),
|
||||||
Message: message,
|
Message: message,
|
||||||
Tags: []string{tag},
|
Tags: []string{tag},
|
||||||
Priority: provider.Priority,
|
Priority: provider.Priority,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
|
@ -92,10 +92,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -59,13 +59,13 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
//
|
//
|
||||||
// Relevant: https://docs.opsgenie.com/docs/alert-api
|
// Relevant: https://docs.opsgenie.com/docs/alert-api
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
err := provider.createAlert(endpoint, alert, result, resolved)
|
err := provider.createAlert(ep, alert, result, resolved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if resolved {
|
if resolved {
|
||||||
err = provider.closeAlert(endpoint, alert)
|
err = provider.closeAlert(ep, alert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -75,20 +75,20 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||||
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
|
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
|
||||||
alert.ResolveKey = ""
|
alert.ResolveKey = ""
|
||||||
} else {
|
} else {
|
||||||
alert.ResolveKey = provider.alias(buildKey(endpoint))
|
alert.ResolveKey = provider.alias(buildKey(ep))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) createAlert(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) createAlert(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
payload := provider.buildCreateRequestBody(endpoint, alert, result, resolved)
|
payload := provider.buildCreateRequestBody(ep, alert, result, resolved)
|
||||||
return provider.sendRequest(restAPI, http.MethodPost, payload)
|
return provider.sendRequest(restAPI, http.MethodPost, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) closeAlert(endpoint *core.Endpoint, alert *alert.Alert) error {
|
func (provider *AlertProvider) closeAlert(ep *endpoint.Endpoint, alert *alert.Alert) error {
|
||||||
payload := provider.buildCloseRequestBody(endpoint, alert)
|
payload := provider.buildCloseRequestBody(ep, alert)
|
||||||
url := restAPI + "/" + provider.alias(buildKey(endpoint)) + "/close?identifierType=alias"
|
url := restAPI + "/" + provider.alias(buildKey(ep)) + "/close?identifierType=alias"
|
||||||
return provider.sendRequest(url, http.MethodPost, payload)
|
return provider.sendRequest(url, http.MethodPost, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,17 +115,17 @@ func (provider *AlertProvider) sendRequest(url, method string, payload interface
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) alertCreateRequest {
|
func (provider *AlertProvider) buildCreateRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) alertCreateRequest {
|
||||||
var message, description string
|
var message, description string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription())
|
||||||
description = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
description = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("%s - %s", endpoint.Name, alert.GetDescription())
|
message = fmt.Sprintf("%s - %s", ep.Name, alert.GetDescription())
|
||||||
description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
if endpoint.Group != "" {
|
if ep.Group != "" {
|
||||||
message = fmt.Sprintf("[%s] %s", endpoint.Group, message)
|
message = fmt.Sprintf("[%s] %s", ep.Group, message)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
@ -138,10 +138,10 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
|
||||||
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
description = description + "\n" + formattedConditionResults
|
description = description + "\n" + formattedConditionResults
|
||||||
key := buildKey(endpoint)
|
key := buildKey(ep)
|
||||||
details := map[string]string{
|
details := map[string]string{
|
||||||
"endpoint:url": endpoint.URL,
|
"endpoint:url": ep.URL,
|
||||||
"endpoint:group": endpoint.Group,
|
"endpoint:group": ep.Group,
|
||||||
"result:hostname": result.Hostname,
|
"result:hostname": result.Hostname,
|
||||||
"result:ip": result.IP,
|
"result:ip": result.IP,
|
||||||
"result:dns_code": result.DNSRCode,
|
"result:dns_code": result.DNSRCode,
|
||||||
|
@ -167,10 +167,10 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildCloseRequestBody(endpoint *core.Endpoint, alert *alert.Alert) alertCloseRequest {
|
func (provider *AlertProvider) buildCloseRequestBody(ep *endpoint.Endpoint, alert *alert.Alert) alertCloseRequest {
|
||||||
return alertCloseRequest{
|
return alertCloseRequest{
|
||||||
Source: buildKey(endpoint),
|
Source: buildKey(ep),
|
||||||
Note: fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()),
|
Note: fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,12 +211,12 @@ func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildKey(endpoint *core.Endpoint) string {
|
func buildKey(ep *endpoint.Endpoint) string {
|
||||||
name := toKebabCase(endpoint.Name)
|
name := toKebabCase(ep.Name)
|
||||||
if endpoint.Group == "" {
|
if ep.Group == "" {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
return toKebabCase(endpoint.Group) + "-" + name
|
return toKebabCase(ep.Group) + "-" + name
|
||||||
}
|
}
|
||||||
|
|
||||||
func toKebabCase(val string) string {
|
func toKebabCase(val string) string {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,10 +79,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -106,8 +106,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||||
Name string
|
Name string
|
||||||
Provider *AlertProvider
|
Provider *AlertProvider
|
||||||
Alert *alert.Alert
|
Alert *alert.Alert
|
||||||
Endpoint *core.Endpoint
|
Endpoint *endpoint.Endpoint
|
||||||
Result *core.Result
|
Result *endpoint.Result
|
||||||
Resolved bool
|
Resolved bool
|
||||||
want alertCreateRequest
|
want alertCreateRequest
|
||||||
}{
|
}{
|
||||||
|
@ -115,8 +115,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||||
Name: "missing all params (unresolved)",
|
Name: "missing all params (unresolved)",
|
||||||
Provider: &AlertProvider{},
|
Provider: &AlertProvider{},
|
||||||
Alert: &alert.Alert{},
|
Alert: &alert.Alert{},
|
||||||
Endpoint: &core.Endpoint{},
|
Endpoint: &endpoint.Endpoint{},
|
||||||
Result: &core.Result{},
|
Result: &endpoint.Result{},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
want: alertCreateRequest{
|
want: alertCreateRequest{
|
||||||
Message: " - ",
|
Message: " - ",
|
||||||
|
@ -133,8 +133,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||||
Name: "missing all params (resolved)",
|
Name: "missing all params (resolved)",
|
||||||
Provider: &AlertProvider{},
|
Provider: &AlertProvider{},
|
||||||
Alert: &alert.Alert{},
|
Alert: &alert.Alert{},
|
||||||
Endpoint: &core.Endpoint{},
|
Endpoint: &endpoint.Endpoint{},
|
||||||
Result: &core.Result{},
|
Result: &endpoint.Result{},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
want: alertCreateRequest{
|
want: alertCreateRequest{
|
||||||
Message: "RESOLVED: - ",
|
Message: "RESOLVED: - ",
|
||||||
|
@ -154,11 +154,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||||
Description: &description,
|
Description: &description,
|
||||||
FailureThreshold: 3,
|
FailureThreshold: 3,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "my super app",
|
Name: "my super app",
|
||||||
},
|
},
|
||||||
Result: &core.Result{
|
Result: &endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
@ -194,11 +194,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||||
Description: &description,
|
Description: &description,
|
||||||
SuccessThreshold: 4,
|
SuccessThreshold: 4,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "my mega app",
|
Name: "my mega app",
|
||||||
},
|
},
|
||||||
Result: &core.Result{
|
Result: &endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
@ -226,17 +226,17 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||||
Description: &description,
|
Description: &description,
|
||||||
FailureThreshold: 6,
|
FailureThreshold: 6,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "my app",
|
Name: "my app",
|
||||||
Group: "end game",
|
Group: "end game",
|
||||||
URL: "https://my.go/app",
|
URL: "https://my.go/app",
|
||||||
},
|
},
|
||||||
Result: &core.Result{
|
Result: &endpoint.Result{
|
||||||
HTTPStatus: 400,
|
HTTPStatus: 400,
|
||||||
Hostname: "my.go",
|
Hostname: "my.go",
|
||||||
Errors: []string{"error 01", "error 02"},
|
Errors: []string{"error 01", "error 02"},
|
||||||
Success: false,
|
Success: false,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: false,
|
Success: false,
|
||||||
|
@ -279,14 +279,14 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
|
||||||
Name string
|
Name string
|
||||||
Provider *AlertProvider
|
Provider *AlertProvider
|
||||||
Alert *alert.Alert
|
Alert *alert.Alert
|
||||||
Endpoint *core.Endpoint
|
Endpoint *endpoint.Endpoint
|
||||||
want alertCloseRequest
|
want alertCloseRequest
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "Missing all values",
|
Name: "Missing all values",
|
||||||
Provider: &AlertProvider{},
|
Provider: &AlertProvider{},
|
||||||
Alert: &alert.Alert{},
|
Alert: &alert.Alert{},
|
||||||
Endpoint: &core.Endpoint{},
|
Endpoint: &endpoint.Endpoint{},
|
||||||
want: alertCloseRequest{
|
want: alertCloseRequest{
|
||||||
Source: "",
|
Source: "",
|
||||||
Note: "RESOLVED: - ",
|
Note: "RESOLVED: - ",
|
||||||
|
@ -298,7 +298,7 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
|
||||||
Alert: &alert.Alert{
|
Alert: &alert.Alert{
|
||||||
Description: &description,
|
Description: &description,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "endpoint name",
|
Name: "endpoint name",
|
||||||
},
|
},
|
||||||
want: alertCloseRequest{
|
want: alertCloseRequest{
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
//
|
//
|
||||||
// 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) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -101,19 +101,19 @@ type Payload struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, eventAction, resolveKey string
|
var message, eventAction, resolveKey string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
eventAction = "resolve"
|
eventAction = "resolve"
|
||||||
resolveKey = alert.ResolveKey
|
resolveKey = alert.ResolveKey
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
eventAction = "trigger"
|
eventAction = "trigger"
|
||||||
resolveKey = ""
|
resolveKey = ""
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
RoutingKey: provider.getIntegrationKeyForGroup(endpoint.Group),
|
RoutingKey: provider.getIntegrationKeyForGroup(ep.Group),
|
||||||
DedupKey: resolveKey,
|
DedupKey: resolveKey,
|
||||||
EventAction: eventAction,
|
EventAction: eventAction,
|
||||||
Payload: Payload{
|
Payload: Payload{
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,10 +115,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -161,7 +161,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(&core.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &core.Result{}, scenario.Resolved)
|
body := scenario.Provider.buildRequestBody(&endpoint.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &endpoint.Result{}, scenario.Resolved)
|
||||||
if string(body) != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import (
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the interface that each provider should implement
|
// AlertProvider is the interface that each provider should implement
|
||||||
|
@ -33,7 +33,7 @@ type AlertProvider interface {
|
||||||
GetDefaultAlert() *alert.Alert
|
GetDefaultAlert() *alert.Alert
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error
|
Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline
|
// ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
// Reference doc for pushover: https://pushover.net/api
|
// Reference doc for pushover: https://pushover.net/api
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -81,12 +81,12 @@ type Body struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
Token: provider.ApplicationToken,
|
Token: provider.ApplicationToken,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,10 +95,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -150,10 +150,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Slack
|
// AlertProvider is the configuration necessary for sending an alert using Slack
|
||||||
|
@ -42,9 +42,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -81,13 +81,13 @@ type Field struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -142,7 +142,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
NoConditions bool
|
NoConditions bool
|
||||||
Resolved bool
|
Resolved bool
|
||||||
|
@ -151,7 +151,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
|
@ -159,7 +159,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "triggered-with-group",
|
Name: "triggered-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
|
@ -168,7 +168,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
Name: "triggered-with-no-conditions",
|
Name: "triggered-with-no-conditions",
|
||||||
NoConditions: true,
|
NoConditions: true,
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\"}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\"}]}",
|
||||||
|
@ -176,7 +176,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
|
@ -184,7 +184,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "resolved-with-group",
|
Name: "resolved-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
|
@ -192,9 +192,9 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
}
|
}
|
||||||
|
@ -202,7 +202,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: conditionResults,
|
ConditionResults: conditionResults,
|
||||||
},
|
},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Teams
|
// AlertProvider is the configuration necessary for sending an alert using Teams
|
||||||
|
@ -21,7 +21,7 @@ type AlertProvider struct {
|
||||||
|
|
||||||
// Overrides is a list of Override that may be prioritized over the default configuration
|
// Overrides is a list of Override that may be prioritized over the default configuration
|
||||||
Overrides []Override `yaml:"overrides,omitempty"`
|
Overrides []Override `yaml:"overrides,omitempty"`
|
||||||
|
|
||||||
// Title is the title of the message that will be sent
|
// Title is the title of the message that will be sent
|
||||||
Title string `yaml:"title,omitempty"`
|
Title string `yaml:"title,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -47,9 +47,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -81,13 +81,13 @@ type Section struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -172,17 +172,17 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{ConditionResults: conditionResults},
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
if string(body) != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultAPIURL = "https://api.telegram.org"
|
const defaultAPIURL = "https://api.telegram.org"
|
||||||
|
@ -36,8 +36,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
apiURL := provider.APIURL
|
apiURL := provider.APIURL
|
||||||
if apiURL == "" {
|
if apiURL == "" {
|
||||||
apiURL = defaultAPIURL
|
apiURL = defaultAPIURL
|
||||||
|
@ -66,12 +66,12 @@ type Body struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message 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— ", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -89,10 +89,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -145,17 +145,17 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{ConditionResults: conditionResults},
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
if string(body) != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Twilio
|
// AlertProvider is the configuration necessary for sending an alert using Twilio
|
||||||
|
@ -30,8 +30,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
|
||||||
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer)
|
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -51,12 +51,12 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
return url.Values{
|
return url.Values{
|
||||||
"To": {provider.To},
|
"To": {provider.To},
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
||||||
|
@ -51,10 +51,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
|
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
|
@ -19,7 +19,7 @@ func TestBadge(t *testing.T) {
|
||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
|
@ -34,8 +34,8 @@ func TestBadge(t *testing.T) {
|
||||||
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
|
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
|
||||||
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
|
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
|
||||||
|
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
@ -218,30 +218,30 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
firstCondition = core.Condition("[STATUS] == 200")
|
firstCondition = endpoint.Condition("[STATUS] == 200")
|
||||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
|
||||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||||
)
|
)
|
||||||
|
|
||||||
firstTestEndpoint := core.Endpoint{
|
firstTestEndpoint := endpoint.Endpoint{
|
||||||
Name: "a",
|
Name: "a",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
UIConfig: ui.GetDefaultConfig(),
|
UIConfig: ui.GetDefaultConfig(),
|
||||||
}
|
}
|
||||||
secondTestEndpoint := core.Endpoint{
|
secondTestEndpoint := endpoint.Endpoint{
|
||||||
Name: "b",
|
Name: "b",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
|
@ -255,10 +255,10 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||||
}
|
}
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
Endpoints: []*endpoint.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
||||||
}
|
}
|
||||||
|
|
||||||
testSuccessfulResult := core.Result{
|
testSuccessfulResult := endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
|
@ -268,7 +268,7 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Duration: 150 * time.Millisecond,
|
Duration: 150 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
|
@ -17,7 +17,7 @@ func TestResponseTimeChart(t *testing.T) {
|
||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
|
@ -28,8 +28,8 @@ func TestResponseTimeChart(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/remote"
|
"github.com/TwiN/gatus/v5/config/remote"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
|
@ -51,11 +51,11 @@ func EndpointStatuses(cfg *config.Config) fiber.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*core.EndpointStatus, error) {
|
func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*endpoint.Status, error) {
|
||||||
if remoteConfig == nil || len(remoteConfig.Instances) == 0 {
|
if remoteConfig == nil || len(remoteConfig.Instances) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var endpointStatusesFromAllRemotes []*core.EndpointStatus
|
var endpointStatusesFromAllRemotes []*endpoint.Status
|
||||||
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
|
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
|
||||||
for _, instance := range remoteConfig.Instances {
|
for _, instance := range remoteConfig.Instances {
|
||||||
response, err := httpClient.Get(instance.URL)
|
response, err := httpClient.Get(instance.URL)
|
||||||
|
@ -68,7 +68,7 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*cor
|
||||||
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var endpointStatuses []*core.EndpointStatus
|
var endpointStatuses []*endpoint.Status
|
||||||
if err = json.Unmarshal(body, &endpointStatuses); err != nil {
|
if err = json.Unmarshal(body, &endpointStatuses); err != nil {
|
||||||
_ = response.Body.Close()
|
_ = response.Body.Close()
|
||||||
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
||||||
|
@ -83,7 +83,7 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*cor
|
||||||
return endpointStatusesFromAllRemotes, nil
|
return endpointStatusesFromAllRemotes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointStatus retrieves a single core.EndpointStatus by group and endpoint name
|
// EndpointStatus retrieves a single endpoint.Status by group and endpoint name
|
||||||
func EndpointStatus(c *fiber.Ctx) error {
|
func EndpointStatus(c *fiber.Ctx) error {
|
||||||
page, pageSize := extractPageAndPageSizeFromRequest(c)
|
page, pageSize := extractPageAndPageSizeFromRequest(c)
|
||||||
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
|
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
|
@ -16,19 +16,19 @@ import (
|
||||||
var (
|
var (
|
||||||
timestamp = time.Now()
|
timestamp = time.Now()
|
||||||
|
|
||||||
testEndpoint = core.Endpoint{
|
testEndpoint = endpoint.Endpoint{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Group: "group",
|
Group: "group",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{core.Condition("[STATUS] == 200"), core.Condition("[RESPONSE_TIME] < 500"), core.Condition("[CERTIFICATE_EXPIRATION] < 72h")},
|
Conditions: []endpoint.Condition{endpoint.Condition("[STATUS] == 200"), endpoint.Condition("[RESPONSE_TIME] < 500"), endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
}
|
}
|
||||||
testSuccessfulResult = core.Result{
|
testSuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
|
@ -38,7 +38,7 @@ var (
|
||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
Duration: 150 * time.Millisecond,
|
Duration: 150 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
@ -53,7 +53,7 @@ var (
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testUnsuccessfulResult = core.Result{
|
testUnsuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
|
@ -63,7 +63,7 @@ var (
|
||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
Duration: 750 * time.Millisecond,
|
Duration: 750 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
@ -85,7 +85,7 @@ func TestEndpointStatus(t *testing.T) {
|
||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
|
@ -96,8 +96,8 @@ func TestEndpointStatus(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
|
@ -41,7 +41,7 @@ func CreateExternalEndpointResult(cfg *config.Config) fiber.Handler {
|
||||||
return c.Status(401).SendString("invalid token")
|
return c.Status(401).SendString("invalid token")
|
||||||
}
|
}
|
||||||
// Persist the result in the storage
|
// Persist the result in the storage
|
||||||
result := &core.Result{
|
result := &endpoint.Result{
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Success: c.QueryBool("success"),
|
Success: c.QueryBool("success"),
|
||||||
Errors: []string{},
|
Errors: []string{},
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/maintenance"
|
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
)
|
)
|
||||||
|
@ -22,7 +22,7 @@ func TestCreateExternalEndpointResult(t *testing.T) {
|
||||||
Alerting: &alerting.Config{
|
Alerting: &alerting.Config{
|
||||||
Discord: &discord.AlertProvider{},
|
Discord: &discord.AlertProvider{},
|
||||||
},
|
},
|
||||||
ExternalEndpoints: []*core.ExternalEndpoint{
|
ExternalEndpoints: []*endpoint.ExternalEndpoint{
|
||||||
{
|
{
|
||||||
Name: "n",
|
Name: "n",
|
||||||
Group: "g",
|
Group: "g",
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/ui"
|
"github.com/TwiN/gatus/v5/config/ui"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
|
@ -20,7 +20,7 @@ func TestSinglePageApplication(t *testing.T) {
|
||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
|
@ -34,8 +34,8 @@ func TestSinglePageApplication(t *testing.T) {
|
||||||
Title: "example-title",
|
Title: "example-title",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
|
|
@ -16,11 +16,16 @@ import (
|
||||||
"github.com/TwiN/gocache/v2"
|
"github.com/TwiN/gocache/v2"
|
||||||
"github.com/TwiN/whois"
|
"github.com/TwiN/whois"
|
||||||
"github.com/ishidawataru/sctp"
|
"github.com/ishidawataru/sctp"
|
||||||
|
"github.com/miekg/dns"
|
||||||
ping "github.com/prometheus-community/pro-bing"
|
ping "github.com/prometheus-community/pro-bing"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnsPort = 53
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// injectedHTTPClient is used for testing purposes
|
// injectedHTTPClient is used for testing purposes
|
||||||
injectedHTTPClient *http.Client
|
injectedHTTPClient *http.Client
|
||||||
|
@ -291,6 +296,49 @@ func QueryWebSocket(address, body string, config *Config) (bool, []byte, error)
|
||||||
return true, msg[:n], nil
|
return true, msg[:n], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func QueryDNS(queryType, queryName, url string) (connected bool, dnsRcode string, body []byte, err error) {
|
||||||
|
if !strings.Contains(url, ":") {
|
||||||
|
url = fmt.Sprintf("%s:%d", url, dnsPort)
|
||||||
|
}
|
||||||
|
queryTypeAsUint16 := dns.StringToType[queryType]
|
||||||
|
c := new(dns.Client)
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(queryName, queryTypeAsUint16)
|
||||||
|
r, _, err := c.Exchange(m, url)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", nil, err
|
||||||
|
}
|
||||||
|
connected = true
|
||||||
|
dnsRcode = dns.RcodeToString[r.Rcode]
|
||||||
|
for _, rr := range r.Answer {
|
||||||
|
switch rr.Header().Rrtype {
|
||||||
|
case dns.TypeA:
|
||||||
|
if a, ok := rr.(*dns.A); ok {
|
||||||
|
body = []byte(a.A.String())
|
||||||
|
}
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
if aaaa, ok := rr.(*dns.AAAA); ok {
|
||||||
|
body = []byte(aaaa.AAAA.String())
|
||||||
|
}
|
||||||
|
case dns.TypeCNAME:
|
||||||
|
if cname, ok := rr.(*dns.CNAME); ok {
|
||||||
|
body = []byte(cname.Target)
|
||||||
|
}
|
||||||
|
case dns.TypeMX:
|
||||||
|
if mx, ok := rr.(*dns.MX); ok {
|
||||||
|
body = []byte(mx.Mx)
|
||||||
|
}
|
||||||
|
case dns.TypeNS:
|
||||||
|
if ns, ok := rr.(*dns.NS); ok {
|
||||||
|
body = []byte(ns.Ns)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
body = []byte("query type is not supported yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return connected, dnsRcode, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
|
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
|
||||||
func InjectHTTPClient(httpClient *http.Client) {
|
func InjectHTTPClient(httpClient *http.Client) {
|
||||||
injectedHTTPClient = httpClient
|
injectedHTTPClient = httpClient
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
|
"github.com/TwiN/gatus/v5/pattern"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -334,3 +336,97 @@ func TestTlsRenegotiation(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQueryDNS(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputDNS dns.Config
|
||||||
|
inputURL string
|
||||||
|
expectedDNSCode string
|
||||||
|
expectedBody string
|
||||||
|
isErrExpected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test Config with type A",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "A",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "93.184.215.14",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type AAAA",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "AAAA",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "2606:2800:21f:cb07:6820:80da:af6b:8b2c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type CNAME",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "CNAME",
|
||||||
|
QueryName: "en.wikipedia.org.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "dyna.wikimedia.org.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type MX",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "MX",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: ".",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type NS",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "NS",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "*.iana-servers.net.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with fake type and retrieve error",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "B",
|
||||||
|
QueryName: "example",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
isErrExpected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
_, dnsRCode, body, err := QueryDNS(test.inputDNS.QueryType, test.inputDNS.QueryName, test.inputURL)
|
||||||
|
if test.isErrExpected && err == nil {
|
||||||
|
t.Errorf("there should be an error")
|
||||||
|
}
|
||||||
|
if dnsRCode != test.expectedDNSCode {
|
||||||
|
t.Errorf("expected DNSRCode to be %s, got %s", test.expectedDNSCode, dnsRCode)
|
||||||
|
}
|
||||||
|
if test.inputDNS.QueryType == "NS" {
|
||||||
|
// Because there are often multiple nameservers backing a single domain, we'll only look at the suffix
|
||||||
|
if !pattern.Match(test.expectedBody, string(body)) {
|
||||||
|
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if string(body) != test.expectedBody {
|
||||||
|
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,14 +15,13 @@ import (
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider"
|
"github.com/TwiN/gatus/v5/alerting/provider"
|
||||||
"github.com/TwiN/gatus/v5/config/connectivity"
|
"github.com/TwiN/gatus/v5/config/connectivity"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/maintenance"
|
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||||
"github.com/TwiN/gatus/v5/config/remote"
|
"github.com/TwiN/gatus/v5/config/remote"
|
||||||
"github.com/TwiN/gatus/v5/config/ui"
|
"github.com/TwiN/gatus/v5/config/ui"
|
||||||
"github.com/TwiN/gatus/v5/config/web"
|
"github.com/TwiN/gatus/v5/config/web"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/security"
|
"github.com/TwiN/gatus/v5/security"
|
||||||
"github.com/TwiN/gatus/v5/storage"
|
"github.com/TwiN/gatus/v5/storage"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -74,10 +73,10 @@ type Config struct {
|
||||||
Alerting *alerting.Config `yaml:"alerting,omitempty"`
|
Alerting *alerting.Config `yaml:"alerting,omitempty"`
|
||||||
|
|
||||||
// Endpoints is the list of endpoints to monitor
|
// Endpoints is the list of endpoints to monitor
|
||||||
Endpoints []*core.Endpoint `yaml:"endpoints,omitempty"`
|
Endpoints []*endpoint.Endpoint `yaml:"endpoints,omitempty"`
|
||||||
|
|
||||||
// ExternalEndpoints is the list of all external endpoints
|
// ExternalEndpoints is the list of all external endpoints
|
||||||
ExternalEndpoints []*core.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
|
ExternalEndpoints []*endpoint.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
|
||||||
|
|
||||||
// Storage is the configuration for how the data is stored
|
// Storage is the configuration for how the data is stored
|
||||||
Storage *storage.Config `yaml:"storage,omitempty"`
|
Storage *storage.Config `yaml:"storage,omitempty"`
|
||||||
|
@ -102,20 +101,20 @@ type Config struct {
|
||||||
lastFileModTime time.Time // last modification time
|
lastFileModTime time.Time // last modification time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
|
func (config *Config) GetEndpointByKey(key string) *endpoint.Endpoint {
|
||||||
for i := 0; i < len(config.Endpoints); i++ {
|
for i := 0; i < len(config.Endpoints); i++ {
|
||||||
ep := config.Endpoints[i]
|
ep := config.Endpoints[i]
|
||||||
if util.ConvertGroupAndEndpointNameToKey(ep.Group, ep.Name) == key {
|
if ep.Key() == key {
|
||||||
return ep
|
return ep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) GetExternalEndpointByKey(key string) *core.ExternalEndpoint {
|
func (config *Config) GetExternalEndpointByKey(key string) *endpoint.ExternalEndpoint {
|
||||||
for i := 0; i < len(config.ExternalEndpoints); i++ {
|
for i := 0; i < len(config.ExternalEndpoints); i++ {
|
||||||
ee := config.ExternalEndpoints[i]
|
ee := config.ExternalEndpoints[i]
|
||||||
if util.ConvertGroupAndEndpointNameToKey(ee.Group, ee.Name) == key {
|
if ee.Key() == key {
|
||||||
return ee
|
return ee
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -339,12 +338,12 @@ func validateWebConfig(config *Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateEndpointsConfig(config *Config) error {
|
func validateEndpointsConfig(config *Config) error {
|
||||||
for _, endpoint := range config.Endpoints {
|
for _, ep := range config.Endpoints {
|
||||||
if config.Debug {
|
if config.Debug {
|
||||||
log.Printf("[config.validateEndpointsConfig] Validating endpoint '%s'", endpoint.Name)
|
log.Printf("[config.validateEndpointsConfig] Validating endpoint '%s'", ep.Name)
|
||||||
}
|
}
|
||||||
if err := endpoint.ValidateAndSetDefaults(); err != nil {
|
if err := ep.ValidateAndSetDefaults(); err != nil {
|
||||||
return fmt.Errorf("invalid endpoint %s: %w", endpoint.DisplayName(), err)
|
return fmt.Errorf("invalid endpoint %s: %w", ep.DisplayName(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
|
log.Printf("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
|
||||||
|
@ -352,12 +351,12 @@ func validateEndpointsConfig(config *Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateExternalEndpointsConfig(config *Config) error {
|
func validateExternalEndpointsConfig(config *Config) error {
|
||||||
for _, externalEndpoint := range config.ExternalEndpoints {
|
for _, ee := range config.ExternalEndpoints {
|
||||||
if config.Debug {
|
if config.Debug {
|
||||||
log.Printf("[config.validateExternalEndpointsConfig] Validating external endpoint '%s'", externalEndpoint.Name)
|
log.Printf("[config.validateExternalEndpointsConfig] Validating external endpoint '%s'", ee.Name)
|
||||||
}
|
}
|
||||||
if err := externalEndpoint.ValidateAndSetDefaults(); err != nil {
|
if err := ee.ValidateAndSetDefaults(); err != nil {
|
||||||
return fmt.Errorf("invalid external endpoint %s: %w", externalEndpoint.DisplayName(), err)
|
return fmt.Errorf("invalid external endpoint %s: %w", ee.DisplayName(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[config.validateExternalEndpointsConfig] Validated %d external endpoints", len(config.ExternalEndpoints))
|
log.Printf("[config.validateExternalEndpointsConfig] Validated %d external endpoints", len(config.ExternalEndpoints))
|
||||||
|
@ -381,9 +380,9 @@ func validateSecurityConfig(config *Config) error {
|
||||||
|
|
||||||
// validateAlertingConfig validates the alerting configuration
|
// validateAlertingConfig validates the alerting configuration
|
||||||
// Note that the alerting configuration has to be validated before the endpoint configuration, because the default alert
|
// Note that the alerting configuration has to be validated before the endpoint configuration, because the default alert
|
||||||
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before core.Endpoint.ValidateAndSetDefaults()
|
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before endpoint.Endpoint.ValidateAndSetDefaults()
|
||||||
// sets the default alert values when none are set.
|
// sets the default alert values when none are set.
|
||||||
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.Endpoint, debug bool) {
|
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoint.Endpoint, debug bool) {
|
||||||
if alertingConfig == nil {
|
if alertingConfig == nil {
|
||||||
log.Printf("[config.validateAlertingConfig] Alerting is not configured")
|
log.Printf("[config.validateAlertingConfig] Alerting is not configured")
|
||||||
return
|
return
|
||||||
|
@ -417,11 +416,11 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
||||||
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 _, endpoint := range endpoints {
|
for _, ep := range endpoints {
|
||||||
for alertIndex, endpointAlert := range endpoint.Alerts {
|
for alertIndex, endpointAlert := range ep.Alerts {
|
||||||
if alertType == endpointAlert.Type {
|
if alertType == endpointAlert.Type {
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("[config.validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in endpoint=%s", alertIndex, alertType, endpoint.Name)
|
log.Printf("[config.validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in endpoint=%s", alertIndex, alertType, ep.Name)
|
||||||
}
|
}
|
||||||
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
|
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,8 +29,8 @@ import (
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/web"
|
"github.com/TwiN/gatus/v5/config/web"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage"
|
"github.com/TwiN/gatus/v5/storage"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
@ -65,7 +65,7 @@ func TestLoadConfiguration(t *testing.T) {
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website`,
|
- name: website`,
|
||||||
},
|
},
|
||||||
expectedError: core.ErrEndpointWithNoURL,
|
expectedError: endpoint.ErrEndpointWithNoURL,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "config-file-with-endpoint-that-has-no-conditions",
|
name: "config-file-with-endpoint-that-has-no-conditions",
|
||||||
|
@ -76,7 +76,7 @@ endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
url: https://twin.sh/health`,
|
url: https://twin.sh/health`,
|
||||||
},
|
},
|
||||||
expectedError: core.ErrEndpointWithNoCondition,
|
expectedError: endpoint.ErrEndpointWithNoCondition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "config-file",
|
name: "config-file",
|
||||||
|
@ -90,11 +90,11 @@ endpoints:
|
||||||
- "[STATUS] == 200"`,
|
- "[STATUS] == 200"`,
|
||||||
},
|
},
|
||||||
expectedConfig: &Config{
|
expectedConfig: &Config{
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "website",
|
Name: "website",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -136,21 +136,21 @@ endpoints:
|
||||||
- "[BODY].status == UP"`,
|
- "[BODY].status == UP"`,
|
||||||
},
|
},
|
||||||
expectedConfig: &Config{
|
expectedConfig: &Config{
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "one",
|
Name: "one",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
Conditions: []core.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "two",
|
Name: "two",
|
||||||
URL: "https://example.org",
|
URL: "https://example.org",
|
||||||
Conditions: []core.Condition{"len([BODY]) > 0"},
|
Conditions: []endpoint.Condition{"len([BODY]) > 0"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "three",
|
Name: "three",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []core.Condition{"[STATUS] == 200", "[BODY].status == UP"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200", "[BODY].status == UP"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -192,17 +192,17 @@ endpoints:
|
||||||
Discord: &discord.AlertProvider{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"},
|
Discord: &discord.AlertProvider{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"},
|
||||||
Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz"},
|
Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz"},
|
||||||
},
|
},
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "example",
|
Name: "example",
|
||||||
URL: "https://example.org",
|
URL: "https://example.org",
|
||||||
Interval: 5 * time.Second,
|
Interval: 5 * time.Second,
|
||||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -689,8 +689,8 @@ endpoints:
|
||||||
if config.Endpoints[0].Interval != 60*time.Second {
|
if config.Endpoints[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != core.GatusUserAgent {
|
if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != endpoint.GatusUserAgent {
|
||||||
t.Errorf("User-Agent should've been %s because it's the default value, got %s", core.GatusUserAgent, userAgent)
|
t.Errorf("User-Agent should've been %s because it's the default value, got %s", endpoint.GatusUserAgent, userAgent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
|
@ -1,6 +1,8 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
||||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
// ConditionResult result of a Condition
|
// ConditionResult result of a Condition
|
||||||
type ConditionResult struct {
|
type ConditionResult struct {
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
38
config/endpoint/dns/dns.go
Normal file
38
config/endpoint/dns/dns.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrDNSWithNoQueryName is the error with which gatus will panic if a dns is configured without query name
|
||||||
|
ErrDNSWithNoQueryName = errors.New("you must specify a query name in the DNS configuration")
|
||||||
|
|
||||||
|
// ErrDNSWithInvalidQueryType is the error with which gatus will panic if a dns is configured with invalid query type
|
||||||
|
ErrDNSWithInvalidQueryType = errors.New("invalid query type in the DNS configuration")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config for an Endpoint of type DNS
|
||||||
|
type Config struct {
|
||||||
|
// QueryType is the type for the DNS records like A, AAAA, CNAME...
|
||||||
|
QueryType string `yaml:"query-type"`
|
||||||
|
|
||||||
|
// QueryName is the query for DNS
|
||||||
|
QueryName string `yaml:"query-name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Config) ValidateAndSetDefault() error {
|
||||||
|
if len(d.QueryName) == 0 {
|
||||||
|
return ErrDNSWithNoQueryName
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(d.QueryName, ".") {
|
||||||
|
d.QueryName += "."
|
||||||
|
}
|
||||||
|
if _, ok := dns.StringToType[d.QueryType]; !ok {
|
||||||
|
return ErrDNSWithInvalidQueryType
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
27
config/endpoint/dns/dns_test.go
Normal file
27
config/endpoint/dns/dns_test.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_ValidateAndSetDefault(t *testing.T) {
|
||||||
|
dns := &Config{
|
||||||
|
QueryType: "A",
|
||||||
|
QueryName: "",
|
||||||
|
}
|
||||||
|
err := dns.ValidateAndSetDefault()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should've returned an error because endpoint's dns didn't have a query name, which is a mandatory field for dns")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_ValidateAndSetDefaultsWithInvalidDNSQueryType(t *testing.T) {
|
||||||
|
dns := &Config{
|
||||||
|
QueryType: "B",
|
||||||
|
QueryName: "example.com",
|
||||||
|
}
|
||||||
|
err := dns.ValidateAndSetDefault()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should've returned an error because endpoint's dns query type is invalid, it needs to be a valid query name like A, AAAA, CNAME...")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -15,12 +15,13 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
sshconfig "github.com/TwiN/gatus/v5/config/endpoint/ssh"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EndpointType string
|
type Type string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// HostHeader is the name of the header used to specify the host
|
// HostHeader is the name of the header used to specify the host
|
||||||
|
@ -35,17 +36,17 @@ const (
|
||||||
// GatusUserAgent is the default user agent that Gatus uses to send requests.
|
// GatusUserAgent is the default user agent that Gatus uses to send requests.
|
||||||
GatusUserAgent = "Gatus/1.0"
|
GatusUserAgent = "Gatus/1.0"
|
||||||
|
|
||||||
EndpointTypeDNS EndpointType = "DNS"
|
TypeDNS Type = "DNS"
|
||||||
EndpointTypeTCP EndpointType = "TCP"
|
TypeTCP Type = "TCP"
|
||||||
EndpointTypeSCTP EndpointType = "SCTP"
|
TypeSCTP Type = "SCTP"
|
||||||
EndpointTypeUDP EndpointType = "UDP"
|
TypeUDP Type = "UDP"
|
||||||
EndpointTypeICMP EndpointType = "ICMP"
|
TypeICMP Type = "ICMP"
|
||||||
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
|
TypeSTARTTLS Type = "STARTTLS"
|
||||||
EndpointTypeTLS EndpointType = "TLS"
|
TypeTLS Type = "TLS"
|
||||||
EndpointTypeHTTP EndpointType = "HTTP"
|
TypeHTTP Type = "HTTP"
|
||||||
EndpointTypeWS EndpointType = "WEBSOCKET"
|
TypeWS Type = "WEBSOCKET"
|
||||||
EndpointTypeSSH EndpointType = "SSH"
|
TypeSSH Type = "SSH"
|
||||||
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
|
TypeUNKNOWN Type = "UNKNOWN"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -82,12 +83,6 @@ type Endpoint struct {
|
||||||
// URL to send the request to
|
// URL to send the request to
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
|
|
||||||
// DNS is the configuration of DNS monitoring
|
|
||||||
DNS *DNS `yaml:"dns,omitempty"`
|
|
||||||
|
|
||||||
// SSH is the configuration of SSH monitoring.
|
|
||||||
SSH *SSH `yaml:"ssh,omitempty"`
|
|
||||||
|
|
||||||
// Method of the request made to the url of the endpoint
|
// Method of the request made to the url of the endpoint
|
||||||
Method string `yaml:"method,omitempty"`
|
Method string `yaml:"method,omitempty"`
|
||||||
|
|
||||||
|
@ -109,6 +104,12 @@ type Endpoint struct {
|
||||||
// Alerts is the alerting configuration for the endpoint in case of failure
|
// Alerts is the alerting configuration for the endpoint in case of failure
|
||||||
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
||||||
|
|
||||||
|
// DNSConfig is the configuration for DNS monitoring
|
||||||
|
DNSConfig *dns.Config `yaml:"dns,omitempty"`
|
||||||
|
|
||||||
|
// SSH is the configuration for SSH monitoring
|
||||||
|
SSHConfig *sshconfig.Config `yaml:"ssh,omitempty"`
|
||||||
|
|
||||||
// ClientConfig is the configuration of the client used to communicate with the endpoint's target
|
// ClientConfig is the configuration of the client used to communicate with the endpoint's target
|
||||||
ClientConfig *client.Config `yaml:"client,omitempty"`
|
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||||
|
|
||||||
|
@ -123,103 +124,103 @@ type Endpoint struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEnabled returns whether the endpoint is enabled or not
|
// IsEnabled returns whether the endpoint is enabled or not
|
||||||
func (endpoint *Endpoint) IsEnabled() bool {
|
func (e *Endpoint) IsEnabled() bool {
|
||||||
if endpoint.Enabled == nil {
|
if e.Enabled == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return *endpoint.Enabled
|
return *e.Enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type returns the endpoint type
|
// Type returns the endpoint type
|
||||||
func (endpoint *Endpoint) Type() EndpointType {
|
func (e *Endpoint) Type() Type {
|
||||||
switch {
|
switch {
|
||||||
case endpoint.DNS != nil:
|
case e.DNSConfig != nil:
|
||||||
return EndpointTypeDNS
|
return TypeDNS
|
||||||
case strings.HasPrefix(endpoint.URL, "tcp://"):
|
case strings.HasPrefix(e.URL, "tcp://"):
|
||||||
return EndpointTypeTCP
|
return TypeTCP
|
||||||
case strings.HasPrefix(endpoint.URL, "sctp://"):
|
case strings.HasPrefix(e.URL, "sctp://"):
|
||||||
return EndpointTypeSCTP
|
return TypeSCTP
|
||||||
case strings.HasPrefix(endpoint.URL, "udp://"):
|
case strings.HasPrefix(e.URL, "udp://"):
|
||||||
return EndpointTypeUDP
|
return TypeUDP
|
||||||
case strings.HasPrefix(endpoint.URL, "icmp://"):
|
case strings.HasPrefix(e.URL, "icmp://"):
|
||||||
return EndpointTypeICMP
|
return TypeICMP
|
||||||
case strings.HasPrefix(endpoint.URL, "starttls://"):
|
case strings.HasPrefix(e.URL, "starttls://"):
|
||||||
return EndpointTypeSTARTTLS
|
return TypeSTARTTLS
|
||||||
case strings.HasPrefix(endpoint.URL, "tls://"):
|
case strings.HasPrefix(e.URL, "tls://"):
|
||||||
return EndpointTypeTLS
|
return TypeTLS
|
||||||
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
|
case strings.HasPrefix(e.URL, "http://") || strings.HasPrefix(e.URL, "https://"):
|
||||||
return EndpointTypeHTTP
|
return TypeHTTP
|
||||||
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
|
case strings.HasPrefix(e.URL, "ws://") || strings.HasPrefix(e.URL, "wss://"):
|
||||||
return EndpointTypeWS
|
return TypeWS
|
||||||
case strings.HasPrefix(endpoint.URL, "ssh://"):
|
case strings.HasPrefix(e.URL, "ssh://"):
|
||||||
return EndpointTypeSSH
|
return TypeSSH
|
||||||
default:
|
default:
|
||||||
return EndpointTypeUNKNOWN
|
return TypeUNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
|
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
|
||||||
func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
func (e *Endpoint) ValidateAndSetDefaults() error {
|
||||||
if err := validateEndpointNameGroupAndAlerts(endpoint.Name, endpoint.Group, endpoint.Alerts); err != nil {
|
if err := validateEndpointNameGroupAndAlerts(e.Name, e.Group, e.Alerts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(endpoint.URL) == 0 {
|
if len(e.URL) == 0 {
|
||||||
return ErrEndpointWithNoURL
|
return ErrEndpointWithNoURL
|
||||||
}
|
}
|
||||||
if endpoint.ClientConfig == nil {
|
if e.ClientConfig == nil {
|
||||||
endpoint.ClientConfig = client.GetDefaultConfig()
|
e.ClientConfig = client.GetDefaultConfig()
|
||||||
} else {
|
} else {
|
||||||
if err := endpoint.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
if err := e.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.UIConfig == nil {
|
if e.UIConfig == nil {
|
||||||
endpoint.UIConfig = ui.GetDefaultConfig()
|
e.UIConfig = ui.GetDefaultConfig()
|
||||||
} else {
|
} else {
|
||||||
if err := endpoint.UIConfig.ValidateAndSetDefaults(); err != nil {
|
if err := e.UIConfig.ValidateAndSetDefaults(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.Interval == 0 {
|
if e.Interval == 0 {
|
||||||
endpoint.Interval = 1 * time.Minute
|
e.Interval = 1 * time.Minute
|
||||||
}
|
}
|
||||||
if len(endpoint.Method) == 0 {
|
if len(e.Method) == 0 {
|
||||||
endpoint.Method = http.MethodGet
|
e.Method = http.MethodGet
|
||||||
}
|
}
|
||||||
if len(endpoint.Headers) == 0 {
|
if len(e.Headers) == 0 {
|
||||||
endpoint.Headers = make(map[string]string)
|
e.Headers = make(map[string]string)
|
||||||
}
|
}
|
||||||
// Automatically add user agent header if there isn't one specified in the endpoint configuration
|
// Automatically add user agent header if there isn't one specified in the endpoint configuration
|
||||||
if _, userAgentHeaderExists := endpoint.Headers[UserAgentHeader]; !userAgentHeaderExists {
|
if _, userAgentHeaderExists := e.Headers[UserAgentHeader]; !userAgentHeaderExists {
|
||||||
endpoint.Headers[UserAgentHeader] = GatusUserAgent
|
e.Headers[UserAgentHeader] = GatusUserAgent
|
||||||
}
|
}
|
||||||
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
|
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
|
||||||
// and endpoint.GraphQL is set to true
|
// and endpoint.GraphQL is set to true
|
||||||
if _, contentTypeHeaderExists := endpoint.Headers[ContentTypeHeader]; !contentTypeHeaderExists && endpoint.GraphQL {
|
if _, contentTypeHeaderExists := e.Headers[ContentTypeHeader]; !contentTypeHeaderExists && e.GraphQL {
|
||||||
endpoint.Headers[ContentTypeHeader] = "application/json"
|
e.Headers[ContentTypeHeader] = "application/json"
|
||||||
}
|
}
|
||||||
if len(endpoint.Conditions) == 0 {
|
if len(e.Conditions) == 0 {
|
||||||
return ErrEndpointWithNoCondition
|
return ErrEndpointWithNoCondition
|
||||||
}
|
}
|
||||||
for _, c := range endpoint.Conditions {
|
for _, c := range e.Conditions {
|
||||||
if endpoint.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
|
if e.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
|
||||||
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
|
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
|
||||||
}
|
}
|
||||||
if err := c.Validate(); err != nil {
|
if err := c.Validate(); err != nil {
|
||||||
return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err)
|
return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.DNS != nil {
|
if e.DNSConfig != nil {
|
||||||
return endpoint.DNS.validateAndSetDefault()
|
return e.DNSConfig.ValidateAndSetDefault()
|
||||||
}
|
}
|
||||||
if endpoint.SSH != nil {
|
if e.SSHConfig != nil {
|
||||||
return endpoint.SSH.validate()
|
return e.SSHConfig.Validate()
|
||||||
}
|
}
|
||||||
if endpoint.Type() == EndpointTypeUNKNOWN {
|
if e.Type() == TypeUNKNOWN {
|
||||||
return ErrUnknownEndpointType
|
return ErrUnknownEndpointType
|
||||||
}
|
}
|
||||||
// Make sure that the request can be created
|
// Make sure that the request can be created
|
||||||
_, err := http.NewRequest(endpoint.Method, endpoint.URL, bytes.NewBuffer([]byte(endpoint.Body)))
|
_, err := http.NewRequest(e.Method, e.URL, bytes.NewBuffer([]byte(e.Body)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -227,35 +228,35 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayName returns an identifier made up of the Name and, if not empty, the Group.
|
// DisplayName returns an identifier made up of the Name and, if not empty, the Group.
|
||||||
func (endpoint *Endpoint) DisplayName() string {
|
func (e *Endpoint) DisplayName() string {
|
||||||
if len(endpoint.Group) > 0 {
|
if len(e.Group) > 0 {
|
||||||
return endpoint.Group + "/" + endpoint.Name
|
return e.Group + "/" + e.Name
|
||||||
}
|
}
|
||||||
return endpoint.Name
|
return e.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key returns the unique key for the Endpoint
|
// Key returns the unique key for the Endpoint
|
||||||
func (endpoint *Endpoint) Key() string {
|
func (e *Endpoint) Key() string {
|
||||||
return util.ConvertGroupAndEndpointNameToKey(endpoint.Group, endpoint.Name)
|
return ConvertGroupAndEndpointNameToKey(e.Group, e.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
|
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
|
||||||
// on configuration reload.
|
// on configuration reload.
|
||||||
// More context on https://github.com/TwiN/gatus/issues/536
|
// More context on https://github.com/TwiN/gatus/issues/536
|
||||||
func (endpoint *Endpoint) Close() {
|
func (e *Endpoint) Close() {
|
||||||
if endpoint.Type() == EndpointTypeHTTP {
|
if e.Type() == TypeHTTP {
|
||||||
client.GetHTTPClient(endpoint.ClientConfig).CloseIdleConnections()
|
client.GetHTTPClient(e.ClientConfig).CloseIdleConnections()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
|
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
|
||||||
func (endpoint *Endpoint) EvaluateHealth() *Result {
|
func (e *Endpoint) EvaluateHealth() *Result {
|
||||||
result := &Result{Success: true, Errors: []string{}}
|
result := &Result{Success: true, Errors: []string{}}
|
||||||
// Parse or extract hostname from URL
|
// Parse or extract hostname from URL
|
||||||
if endpoint.DNS != nil {
|
if e.DNSConfig != nil {
|
||||||
result.Hostname = strings.TrimSuffix(endpoint.URL, ":53")
|
result.Hostname = strings.TrimSuffix(e.URL, ":53")
|
||||||
} else {
|
} else {
|
||||||
urlObject, err := url.Parse(endpoint.URL)
|
urlObject, err := url.Parse(e.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
} else {
|
} else {
|
||||||
|
@ -263,11 +264,11 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Retrieve IP if necessary
|
// Retrieve IP if necessary
|
||||||
if endpoint.needsToRetrieveIP() {
|
if e.needsToRetrieveIP() {
|
||||||
endpoint.getIP(result)
|
e.getIP(result)
|
||||||
}
|
}
|
||||||
// Retrieve domain expiration if necessary
|
// Retrieve domain expiration if necessary
|
||||||
if endpoint.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
|
if e.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
|
||||||
var err error
|
var err error
|
||||||
if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil {
|
if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
|
@ -275,37 +276,37 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
||||||
}
|
}
|
||||||
// Call the endpoint (if there's no errors)
|
// Call the endpoint (if there's no errors)
|
||||||
if len(result.Errors) == 0 {
|
if len(result.Errors) == 0 {
|
||||||
endpoint.call(result)
|
e.call(result)
|
||||||
} else {
|
} else {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
// Evaluate the conditions
|
// Evaluate the conditions
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
success := condition.evaluate(result, endpoint.UIConfig.DontResolveFailedConditions)
|
success := condition.evaluate(result, e.UIConfig.DontResolveFailedConditions)
|
||||||
if !success {
|
if !success {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.Timestamp = time.Now()
|
result.Timestamp = time.Now()
|
||||||
// Clean up parameters that we don't need to keep in the results
|
// Clean up parameters that we don't need to keep in the results
|
||||||
if endpoint.UIConfig.HideURL {
|
if e.UIConfig.HideURL {
|
||||||
for errIdx, errorString := range result.Errors {
|
for errIdx, errorString := range result.Errors {
|
||||||
result.Errors[errIdx] = strings.ReplaceAll(errorString, endpoint.URL, "<redacted>")
|
result.Errors[errIdx] = strings.ReplaceAll(errorString, e.URL, "<redacted>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.UIConfig.HideHostname {
|
if e.UIConfig.HideHostname {
|
||||||
for errIdx, errorString := range result.Errors {
|
for errIdx, errorString := range result.Errors {
|
||||||
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
|
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
|
||||||
}
|
}
|
||||||
result.Hostname = ""
|
result.Hostname = ""
|
||||||
}
|
}
|
||||||
if endpoint.UIConfig.HideConditions {
|
if e.UIConfig.HideConditions {
|
||||||
result.ConditionResults = nil
|
result.ConditionResults = nil
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) getIP(result *Result) {
|
func (e *Endpoint) getIP(result *Result) {
|
||||||
if ips, err := net.LookupIP(result.Hostname); err != nil {
|
if ips, err := net.LookupIP(result.Hostname); err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -314,24 +315,28 @@ func (endpoint *Endpoint) getIP(result *Result) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) call(result *Result) {
|
func (e *Endpoint) call(result *Result) {
|
||||||
var request *http.Request
|
var request *http.Request
|
||||||
var response *http.Response
|
var response *http.Response
|
||||||
var err error
|
var err error
|
||||||
var certificate *x509.Certificate
|
var certificate *x509.Certificate
|
||||||
endpointType := endpoint.Type()
|
endpointType := e.Type()
|
||||||
if endpointType == EndpointTypeHTTP {
|
if endpointType == TypeHTTP {
|
||||||
request = endpoint.buildHTTPRequest()
|
request = e.buildHTTPRequest()
|
||||||
}
|
}
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
if endpointType == EndpointTypeDNS {
|
if endpointType == TypeDNS {
|
||||||
endpoint.DNS.query(endpoint.URL, result)
|
result.Connected, result.DNSRCode, result.Body, err = client.QueryDNS(e.DNSConfig.QueryType, e.DNSConfig.QueryName, e.URL)
|
||||||
|
if err != nil {
|
||||||
|
result.AddError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeSTARTTLS || endpointType == EndpointTypeTLS {
|
} else if endpointType == TypeSTARTTLS || endpointType == TypeTLS {
|
||||||
if endpointType == EndpointTypeSTARTTLS {
|
if endpointType == TypeSTARTTLS {
|
||||||
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(endpoint.URL, "starttls://"), endpoint.ClientConfig)
|
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(e.URL, "starttls://"), e.ClientConfig)
|
||||||
} else {
|
} else {
|
||||||
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(endpoint.URL, "tls://"), endpoint.ClientConfig)
|
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(e.URL, "tls://"), e.ClientConfig)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
|
@ -339,39 +344,39 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
||||||
} else if endpointType == EndpointTypeTCP {
|
} else if endpointType == TypeTCP {
|
||||||
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(e.URL, "tcp://"), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeUDP {
|
} else if endpointType == TypeUDP {
|
||||||
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(endpoint.URL, "udp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(e.URL, "udp://"), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeSCTP {
|
} else if endpointType == TypeSCTP {
|
||||||
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(endpoint.URL, "sctp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(e.URL, "sctp://"), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeICMP {
|
} else if endpointType == TypeICMP {
|
||||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
|
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(e.URL, "icmp://"), e.ClientConfig)
|
||||||
} else if endpointType == EndpointTypeWS {
|
} else if endpointType == TypeWS {
|
||||||
result.Connected, result.Body, err = client.QueryWebSocket(endpoint.URL, endpoint.Body, endpoint.ClientConfig)
|
result.Connected, result.Body, err = client.QueryWebSocket(e.URL, e.Body, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeSSH {
|
} else if endpointType == TypeSSH {
|
||||||
var cli *ssh.Client
|
var cli *ssh.Client
|
||||||
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(endpoint.URL, "ssh://"), endpoint.SSH.Username, endpoint.SSH.Password, endpoint.ClientConfig)
|
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(e.URL, "ssh://"), e.SSHConfig.Username, e.SSHConfig.Password, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, endpoint.Body, endpoint.ClientConfig)
|
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, e.Body, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else {
|
} else {
|
||||||
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
response, err = client.GetHTTPClient(e.ClientConfig).Do(request)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
|
@ -385,7 +390,7 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||||
result.HTTPStatus = response.StatusCode
|
result.HTTPStatus = response.StatusCode
|
||||||
result.Connected = response.StatusCode > 0
|
result.Connected = response.StatusCode > 0
|
||||||
// Only read the Body if there's a condition that uses the BodyPlaceholder
|
// Only read the Body if there's a condition that uses the BodyPlaceholder
|
||||||
if endpoint.needsToReadBody() {
|
if e.needsToReadBody() {
|
||||||
result.Body, err = io.ReadAll(response.Body)
|
result.Body, err = io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError("error reading response body:" + err.Error())
|
result.AddError("error reading response body:" + err.Error())
|
||||||
|
@ -394,19 +399,19 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) buildHTTPRequest() *http.Request {
|
func (e *Endpoint) buildHTTPRequest() *http.Request {
|
||||||
var bodyBuffer *bytes.Buffer
|
var bodyBuffer *bytes.Buffer
|
||||||
if endpoint.GraphQL {
|
if e.GraphQL {
|
||||||
graphQlBody := map[string]string{
|
graphQlBody := map[string]string{
|
||||||
"query": endpoint.Body,
|
"query": e.Body,
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(graphQlBody)
|
body, _ := json.Marshal(graphQlBody)
|
||||||
bodyBuffer = bytes.NewBuffer(body)
|
bodyBuffer = bytes.NewBuffer(body)
|
||||||
} else {
|
} else {
|
||||||
bodyBuffer = bytes.NewBuffer([]byte(endpoint.Body))
|
bodyBuffer = bytes.NewBuffer([]byte(e.Body))
|
||||||
}
|
}
|
||||||
request, _ := http.NewRequest(endpoint.Method, endpoint.URL, bodyBuffer)
|
request, _ := http.NewRequest(e.Method, e.URL, bodyBuffer)
|
||||||
for k, v := range endpoint.Headers {
|
for k, v := range e.Headers {
|
||||||
request.Header.Set(k, v)
|
request.Header.Set(k, v)
|
||||||
if k == HostHeader {
|
if k == HostHeader {
|
||||||
request.Host = v
|
request.Host = v
|
||||||
|
@ -416,8 +421,8 @@ func (endpoint *Endpoint) buildHTTPRequest() *http.Request {
|
||||||
}
|
}
|
||||||
|
|
||||||
// needsToReadBody checks if there's any condition that requires the response Body to be read
|
// needsToReadBody checks if there's any condition that requires the response Body to be read
|
||||||
func (endpoint *Endpoint) needsToReadBody() bool {
|
func (e *Endpoint) needsToReadBody() bool {
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
if condition.hasBodyPlaceholder() {
|
if condition.hasBodyPlaceholder() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -426,8 +431,8 @@ func (endpoint *Endpoint) needsToReadBody() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed
|
// needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed
|
||||||
func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
|
func (e *Endpoint) needsToRetrieveDomainExpiration() bool {
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
if condition.hasDomainExpirationPlaceholder() {
|
if condition.hasDomainExpirationPlaceholder() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -436,8 +441,8 @@ func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// needsToRetrieveIP checks if there's any condition that requires an IP lookup
|
// needsToRetrieveIP checks if there's any condition that requires an IP lookup
|
||||||
func (endpoint *Endpoint) needsToRetrieveIP() bool {
|
func (e *Endpoint) needsToRetrieveIP() bool {
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
if condition.hasIPPlaceholder() {
|
if condition.hasIPPlaceholder() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -13,7 +13,9 @@ import (
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ssh"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -279,105 +281,105 @@ func TestEndpoint_IsEnabled(t *testing.T) {
|
||||||
func TestEndpoint_Type(t *testing.T) {
|
func TestEndpoint_Type(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
URL string
|
URL string
|
||||||
DNS *DNS
|
DNS *dns.Config
|
||||||
SSH *SSH
|
SSH *ssh.Config
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
args args
|
args args
|
||||||
want EndpointType
|
want Type
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "8.8.8.8",
|
URL: "8.8.8.8",
|
||||||
DNS: &DNS{
|
DNS: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com",
|
QueryName: "example.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: EndpointTypeDNS,
|
want: TypeDNS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "tcp://127.0.0.1:6379",
|
URL: "tcp://127.0.0.1:6379",
|
||||||
},
|
},
|
||||||
want: EndpointTypeTCP,
|
want: TypeTCP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "icmp://example.com",
|
URL: "icmp://example.com",
|
||||||
},
|
},
|
||||||
want: EndpointTypeICMP,
|
want: TypeICMP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "sctp://example.com",
|
URL: "sctp://example.com",
|
||||||
},
|
},
|
||||||
want: EndpointTypeSCTP,
|
want: TypeSCTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "udp://example.com",
|
URL: "udp://example.com",
|
||||||
},
|
},
|
||||||
want: EndpointTypeUDP,
|
want: TypeUDP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "starttls://smtp.gmail.com:587",
|
URL: "starttls://smtp.gmail.com:587",
|
||||||
},
|
},
|
||||||
want: EndpointTypeSTARTTLS,
|
want: TypeSTARTTLS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "tls://example.com:443",
|
URL: "tls://example.com:443",
|
||||||
},
|
},
|
||||||
want: EndpointTypeTLS,
|
want: TypeTLS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
},
|
},
|
||||||
want: EndpointTypeHTTP,
|
want: TypeHTTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "wss://example.com/",
|
URL: "wss://example.com/",
|
||||||
},
|
},
|
||||||
want: EndpointTypeWS,
|
want: TypeWS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "ws://example.com/",
|
URL: "ws://example.com/",
|
||||||
},
|
},
|
||||||
want: EndpointTypeWS,
|
want: TypeWS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "ssh://example.com:22",
|
URL: "ssh://example.com:22",
|
||||||
SSH: &SSH{
|
SSH: &ssh.Config{
|
||||||
Username: "root",
|
Username: "root",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: EndpointTypeSSH,
|
want: TypeSSH,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "invalid://example.org",
|
URL: "invalid://example.org",
|
||||||
},
|
},
|
||||||
want: EndpointTypeUNKNOWN,
|
want: TypeUNKNOWN,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "no-scheme",
|
URL: "no-scheme",
|
||||||
},
|
},
|
||||||
want: EndpointTypeUNKNOWN,
|
want: TypeUNKNOWN,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(string(tt.want), func(t *testing.T) {
|
t.Run(string(tt.want), func(t *testing.T) {
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
URL: tt.args.URL,
|
URL: tt.args.URL,
|
||||||
DNS: tt.args.DNS,
|
DNSConfig: tt.args.DNS,
|
||||||
}
|
}
|
||||||
if got := endpoint.Type(); got != tt.want {
|
if got := endpoint.Type(); got != tt.want {
|
||||||
t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want)
|
t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want)
|
||||||
|
@ -477,7 +479,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||||
endpoint := &Endpoint{
|
endpoint := &Endpoint{
|
||||||
Name: "dns-test",
|
Name: "dns-test",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
DNS: &DNS{
|
DNSConfig: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com",
|
QueryName: "example.com",
|
||||||
},
|
},
|
||||||
|
@ -487,7 +489,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("did not expect an error, got", err)
|
t.Error("did not expect an error, got", err)
|
||||||
}
|
}
|
||||||
if endpoint.DNS.QueryName != "example.com." {
|
if endpoint.DNSConfig.QueryName != "example.com." {
|
||||||
t.Error("Endpoint.dns.query-name should be formatted with . suffix")
|
t.Error("Endpoint.dns.query-name should be formatted with . suffix")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -503,13 +505,13 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
||||||
name: "fail when has no user",
|
name: "fail when has no user",
|
||||||
username: "",
|
username: "",
|
||||||
password: "password",
|
password: "password",
|
||||||
expectedErr: ErrEndpointWithoutSSHUsername,
|
expectedErr: ssh.ErrEndpointWithoutSSHUsername,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fail when has no password",
|
name: "fail when has no password",
|
||||||
username: "username",
|
username: "username",
|
||||||
password: "",
|
password: "",
|
||||||
expectedErr: ErrEndpointWithoutSSHPassword,
|
expectedErr: ssh.ErrEndpointWithoutSSHPassword,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "success when all fields are set",
|
name: "success when all fields are set",
|
||||||
|
@ -524,7 +526,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
||||||
endpoint := &Endpoint{
|
endpoint := &Endpoint{
|
||||||
Name: "ssh-test",
|
Name: "ssh-test",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
SSH: &SSH{
|
SSHConfig: &ssh.Config{
|
||||||
Username: scenario.username,
|
Username: scenario.username,
|
||||||
Password: scenario.password,
|
Password: scenario.password,
|
||||||
},
|
},
|
||||||
|
@ -763,7 +765,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "example",
|
Name: "example",
|
||||||
URL: "8.8.8.8",
|
URL: "8.8.8.8",
|
||||||
DNS: &DNS{
|
DNSConfig: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com.",
|
QueryName: "example.com.",
|
||||||
},
|
},
|
||||||
|
@ -786,7 +788,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||||
tests := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
endpoint Endpoint
|
endpoint Endpoint
|
||||||
conditions []Condition
|
conditions []Condition
|
||||||
|
@ -797,9 +799,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||||
endpoint: Endpoint{
|
endpoint: Endpoint{
|
||||||
Name: "ssh-success",
|
Name: "ssh-success",
|
||||||
URL: "ssh://localhost",
|
URL: "ssh://localhost",
|
||||||
SSH: &SSH{
|
SSHConfig: &ssh.Config{
|
||||||
Username: "test",
|
Username: "scenario",
|
||||||
Password: "test",
|
Password: "scenario",
|
||||||
},
|
},
|
||||||
Body: "{ \"command\": \"uptime\" }",
|
Body: "{ \"command\": \"uptime\" }",
|
||||||
},
|
},
|
||||||
|
@ -811,9 +813,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||||
endpoint: Endpoint{
|
endpoint: Endpoint{
|
||||||
Name: "ssh-failure",
|
Name: "ssh-failure",
|
||||||
URL: "ssh://localhost",
|
URL: "ssh://localhost",
|
||||||
SSH: &SSH{
|
SSHConfig: &ssh.Config{
|
||||||
Username: "test",
|
Username: "scenario",
|
||||||
Password: "test",
|
Password: "scenario",
|
||||||
},
|
},
|
||||||
Body: "{ \"command\": \"uptime\" }",
|
Body: "{ \"command\": \"uptime\" }",
|
||||||
},
|
},
|
||||||
|
@ -822,13 +824,13 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, scenario := range scenarios {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
test.endpoint.ValidateAndSetDefaults()
|
scenario.endpoint.ValidateAndSetDefaults()
|
||||||
test.endpoint.Conditions = test.conditions
|
scenario.endpoint.Conditions = scenario.conditions
|
||||||
result := test.endpoint.EvaluateHealth()
|
result := scenario.endpoint.EvaluateHealth()
|
||||||
if result.Success != test.success {
|
if result.Success != scenario.success {
|
||||||
t.Errorf("Expected success to be %v, but was %v", test.success, result.Success)
|
t.Errorf("Expected success to be %v, but was %v", scenario.success, result.Success)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Event is something that happens at a specific time
|
// Event is something that happens at a specific time
|
||||||
type Event struct {
|
type Event struct {
|
|
@ -1,6 +1,8 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestNewEventFromResult(t *testing.T) {
|
func TestNewEventFromResult(t *testing.T) {
|
||||||
if event := NewEventFromResult(&Result{Success: true}); event.Type != EventHealthy {
|
if event := NewEventFromResult(&Result{Success: true}); event.Type != EventHealthy {
|
|
@ -1,10 +1,9 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -72,7 +71,7 @@ func (externalEndpoint *ExternalEndpoint) DisplayName() string {
|
||||||
|
|
||||||
// Key returns the unique key for the Endpoint
|
// Key returns the unique key for the Endpoint
|
||||||
func (externalEndpoint *ExternalEndpoint) Key() string {
|
func (externalEndpoint *ExternalEndpoint) Key() string {
|
||||||
return util.ConvertGroupAndEndpointNameToKey(externalEndpoint.Group, externalEndpoint.Name)
|
return ConvertGroupAndEndpointNameToKey(externalEndpoint.Group, externalEndpoint.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToEndpoint converts the ExternalEndpoint to an Endpoint
|
// ToEndpoint converts the ExternalEndpoint to an Endpoint
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package endpoint
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package endpoint
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -12,17 +12,17 @@ var (
|
||||||
ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each SSH endpoint")
|
ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each SSH endpoint")
|
||||||
)
|
)
|
||||||
|
|
||||||
type SSH struct {
|
type Config struct {
|
||||||
Username string `yaml:"username,omitempty"`
|
Username string `yaml:"username,omitempty"`
|
||||||
Password string `yaml:"password,omitempty"`
|
Password string `yaml:"password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate validates the endpoint
|
// Validate the SSH configuration
|
||||||
func (s *SSH) validate() error {
|
func (cfg *Config) Validate() error {
|
||||||
if len(s.Username) == 0 {
|
if len(cfg.Username) == 0 {
|
||||||
return ErrEndpointWithoutSSHUsername
|
return ErrEndpointWithoutSSHUsername
|
||||||
}
|
}
|
||||||
if len(s.Password) == 0 {
|
if len(cfg.Password) == 0 {
|
||||||
return ErrEndpointWithoutSSHPassword
|
return ErrEndpointWithoutSSHPassword
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -6,20 +6,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSSH_validate(t *testing.T) {
|
func TestSSH_validate(t *testing.T) {
|
||||||
ssh := &SSH{}
|
cfg := &Config{}
|
||||||
if err := ssh.validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
t.Error("expected an error")
|
t.Error("expected an error")
|
||||||
} else if !errors.Is(err, ErrEndpointWithoutSSHUsername) {
|
} else if !errors.Is(err, ErrEndpointWithoutSSHUsername) {
|
||||||
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHUsername, err)
|
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHUsername, err)
|
||||||
}
|
}
|
||||||
ssh.Username = "username"
|
cfg.Username = "username"
|
||||||
if err := ssh.validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
t.Error("expected an error")
|
t.Error("expected an error")
|
||||||
} else if !errors.Is(err, ErrEndpointWithoutSSHPassword) {
|
} else if !errors.Is(err, ErrEndpointWithoutSSHPassword) {
|
||||||
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHPassword, err)
|
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHPassword, err)
|
||||||
}
|
}
|
||||||
ssh.Password = "password"
|
cfg.Password = "password"
|
||||||
if err := ssh.validate(); err != nil {
|
if err := cfg.Validate(); err != nil {
|
||||||
t.Errorf("expected no error, got '%v'", err)
|
t.Errorf("expected no error, got '%v'", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,16 +1,14 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "github.com/TwiN/gatus/v5/util"
|
// Status contains the evaluation Results of an Endpoint
|
||||||
|
type Status struct {
|
||||||
// EndpointStatus contains the evaluation Results of an Endpoint
|
|
||||||
type EndpointStatus struct {
|
|
||||||
// Name of the endpoint
|
// Name of the endpoint
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
// Group the endpoint is a part of. Used for grouping multiple endpoints together on the front end.
|
// Group the endpoint is a part of. Used for grouping multiple endpoints together on the front end.
|
||||||
Group string `json:"group,omitempty"`
|
Group string `json:"group,omitempty"`
|
||||||
|
|
||||||
// Key is the key representing the EndpointStatus
|
// Key of the Endpoint
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
|
||||||
// Results is the list of endpoint evaluation results
|
// Results is the list of endpoint evaluation results
|
||||||
|
@ -27,12 +25,12 @@ type EndpointStatus struct {
|
||||||
Uptime *Uptime `json:"-"`
|
Uptime *Uptime `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEndpointStatus creates a new EndpointStatus
|
// NewStatus creates a new Status
|
||||||
func NewEndpointStatus(group, name string) *EndpointStatus {
|
func NewStatus(group, name string) *Status {
|
||||||
return &EndpointStatus{
|
return &Status{
|
||||||
Name: name,
|
Name: name,
|
||||||
Group: group,
|
Group: group,
|
||||||
Key: util.ConvertGroupAndEndpointNameToKey(group, name),
|
Key: ConvertGroupAndEndpointNameToKey(group, name),
|
||||||
Results: make([]*Result, 0),
|
Results: make([]*Result, 0),
|
||||||
Events: make([]*Event, 0),
|
Events: make([]*Event, 0),
|
||||||
Uptime: NewUptime(),
|
Uptime: NewUptime(),
|
19
config/endpoint/status_test.go
Normal file
19
config/endpoint/status_test.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewEndpointStatus(t *testing.T) {
|
||||||
|
ep := &Endpoint{Name: "name", Group: "group"}
|
||||||
|
status := NewStatus(ep.Group, ep.Name)
|
||||||
|
if status.Name != ep.Name {
|
||||||
|
t.Errorf("expected %s, got %s", ep.Name, status.Name)
|
||||||
|
}
|
||||||
|
if status.Group != ep.Group {
|
||||||
|
t.Errorf("expected %s, got %s", ep.Group, status.Group)
|
||||||
|
}
|
||||||
|
if status.Key != "group_name" {
|
||||||
|
t.Errorf("expected %s, got %s", "group_name", status.Key)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ package ui
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
// Config is the UI configuration for core.Endpoint
|
// Config is the UI configuration for endpoint.Endpoint
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// HideConditions whether to hide the condition results on the UI
|
// HideConditions whether to hide the condition results on the UI
|
||||||
HideConditions bool `yaml:"hide-conditions"`
|
HideConditions bool `yaml:"hide-conditions"`
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package endpoint
|
||||||
|
|
||||||
// Uptime is the struct that contains the relevant data for calculating the uptime as well as the uptime itself
|
// Uptime is the struct that contains the relevant data for calculating the uptime as well as the uptime itself
|
||||||
// and some other statistics
|
// and some other statistics
|
|
@ -1 +0,0 @@
|
||||||
TODO: move files from core to here.
|
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/web"
|
"github.com/TwiN/gatus/v5/config/web"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ func TestHandle(t *testing.T) {
|
||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
Port: rand.Intn(65534),
|
Port: rand.Intn(65534),
|
||||||
},
|
},
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
|
@ -64,7 +64,7 @@ func TestHandleTLS(t *testing.T) {
|
||||||
t.Run(scenario.name, func(t *testing.T) {
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Web: &web.Config{Address: "0.0.0.0", Port: rand.Intn(65534), TLS: scenario.tls},
|
Web: &web.Config{Address: "0.0.0.0", Port: rand.Intn(65534), TLS: scenario.tls},
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{Name: "frontend", Group: "core"},
|
{Name: "frontend", Group: "core"},
|
||||||
{Name: "backend", Group: "core"},
|
{Name: "backend", Group: "core"},
|
||||||
},
|
},
|
||||||
|
|
86
core/dns.go
86
core/dns.go
|
@ -1,86 +0,0 @@
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrDNSWithNoQueryName is the error with which gatus will panic if a dns is configured without query name
|
|
||||||
ErrDNSWithNoQueryName = errors.New("you must specify a query name for DNS")
|
|
||||||
|
|
||||||
// ErrDNSWithInvalidQueryType is the error with which gatus will panic if a dns is configured with invalid query type
|
|
||||||
ErrDNSWithInvalidQueryType = errors.New("invalid query type")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dnsPort = 53
|
|
||||||
)
|
|
||||||
|
|
||||||
// DNS is the configuration for a Endpoint of type DNS
|
|
||||||
type DNS struct {
|
|
||||||
// QueryType is the type for the DNS records like A, AAAA, CNAME...
|
|
||||||
QueryType string `yaml:"query-type"`
|
|
||||||
|
|
||||||
// QueryName is the query for DNS
|
|
||||||
QueryName string `yaml:"query-name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DNS) validateAndSetDefault() error {
|
|
||||||
if len(d.QueryName) == 0 {
|
|
||||||
return ErrDNSWithNoQueryName
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(d.QueryName, ".") {
|
|
||||||
d.QueryName += "."
|
|
||||||
}
|
|
||||||
if _, ok := dns.StringToType[d.QueryType]; !ok {
|
|
||||||
return ErrDNSWithInvalidQueryType
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DNS) query(url string, result *Result) {
|
|
||||||
if !strings.Contains(url, ":") {
|
|
||||||
url = fmt.Sprintf("%s:%d", url, dnsPort)
|
|
||||||
}
|
|
||||||
queryType := dns.StringToType[d.QueryType]
|
|
||||||
c := new(dns.Client)
|
|
||||||
m := new(dns.Msg)
|
|
||||||
m.SetQuestion(d.QueryName, queryType)
|
|
||||||
r, _, err := c.Exchange(m, url)
|
|
||||||
if err != nil {
|
|
||||||
result.AddError(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result.Connected = true
|
|
||||||
result.DNSRCode = dns.RcodeToString[r.Rcode]
|
|
||||||
for _, rr := range r.Answer {
|
|
||||||
switch rr.Header().Rrtype {
|
|
||||||
case dns.TypeA:
|
|
||||||
if a, ok := rr.(*dns.A); ok {
|
|
||||||
result.Body = []byte(a.A.String())
|
|
||||||
}
|
|
||||||
case dns.TypeAAAA:
|
|
||||||
if aaaa, ok := rr.(*dns.AAAA); ok {
|
|
||||||
result.Body = []byte(aaaa.AAAA.String())
|
|
||||||
}
|
|
||||||
case dns.TypeCNAME:
|
|
||||||
if cname, ok := rr.(*dns.CNAME); ok {
|
|
||||||
result.Body = []byte(cname.Target)
|
|
||||||
}
|
|
||||||
case dns.TypeMX:
|
|
||||||
if mx, ok := rr.(*dns.MX); ok {
|
|
||||||
result.Body = []byte(mx.Mx)
|
|
||||||
}
|
|
||||||
case dns.TypeNS:
|
|
||||||
if ns, ok := rr.(*dns.NS); ok {
|
|
||||||
result.Body = []byte(ns.Ns)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
result.Body = []byte("query type is not supported yet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
126
core/dns_test.go
126
core/dns_test.go
|
@ -1,126 +0,0 @@
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/pattern"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestIntegrationQuery(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
inputDNS DNS
|
|
||||||
inputURL string
|
|
||||||
expectedDNSCode string
|
|
||||||
expectedBody string
|
|
||||||
isErrExpected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "test DNS with type A",
|
|
||||||
inputDNS: DNS{
|
|
||||||
QueryType: "A",
|
|
||||||
QueryName: "example.com.",
|
|
||||||
},
|
|
||||||
inputURL: "8.8.8.8",
|
|
||||||
expectedDNSCode: "NOERROR",
|
|
||||||
expectedBody: "93.184.215.14",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "test DNS with type AAAA",
|
|
||||||
inputDNS: DNS{
|
|
||||||
QueryType: "AAAA",
|
|
||||||
QueryName: "example.com.",
|
|
||||||
},
|
|
||||||
inputURL: "8.8.8.8",
|
|
||||||
expectedDNSCode: "NOERROR",
|
|
||||||
expectedBody: "2606:2800:21f:cb07:6820:80da:af6b:8b2c",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "test DNS with type CNAME",
|
|
||||||
inputDNS: DNS{
|
|
||||||
QueryType: "CNAME",
|
|
||||||
QueryName: "en.wikipedia.org.",
|
|
||||||
},
|
|
||||||
inputURL: "8.8.8.8",
|
|
||||||
expectedDNSCode: "NOERROR",
|
|
||||||
expectedBody: "dyna.wikimedia.org.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "test DNS with type MX",
|
|
||||||
inputDNS: DNS{
|
|
||||||
QueryType: "MX",
|
|
||||||
QueryName: "example.com.",
|
|
||||||
},
|
|
||||||
inputURL: "8.8.8.8",
|
|
||||||
expectedDNSCode: "NOERROR",
|
|
||||||
expectedBody: ".",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "test DNS with type NS",
|
|
||||||
inputDNS: DNS{
|
|
||||||
QueryType: "NS",
|
|
||||||
QueryName: "example.com.",
|
|
||||||
},
|
|
||||||
inputURL: "8.8.8.8",
|
|
||||||
expectedDNSCode: "NOERROR",
|
|
||||||
expectedBody: "*.iana-servers.net.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "test DNS with fake type and retrieve error",
|
|
||||||
inputDNS: DNS{
|
|
||||||
QueryType: "B",
|
|
||||||
QueryName: "example",
|
|
||||||
},
|
|
||||||
inputURL: "8.8.8.8",
|
|
||||||
isErrExpected: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
dns := test.inputDNS
|
|
||||||
result := &Result{}
|
|
||||||
dns.query(test.inputURL, result)
|
|
||||||
if test.isErrExpected && len(result.Errors) == 0 {
|
|
||||||
t.Errorf("there should be errors")
|
|
||||||
}
|
|
||||||
if result.DNSRCode != test.expectedDNSCode {
|
|
||||||
t.Errorf("expected DNSRCode to be %s, got %s", test.expectedDNSCode, result.DNSRCode)
|
|
||||||
}
|
|
||||||
if test.inputDNS.QueryType == "NS" {
|
|
||||||
// Because there are often multiple nameservers backing a single domain, we'll only look at the suffix
|
|
||||||
if !pattern.Match(test.expectedBody, string(result.Body)) {
|
|
||||||
t.Errorf("got %s, expected result %s,", string(result.Body), test.expectedBody)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if string(result.Body) != test.expectedBody {
|
|
||||||
t.Errorf("got %s, expected result %s,", string(result.Body), test.expectedBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
time.Sleep(5 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNS_validateAndSetDefault(t *testing.T) {
|
|
||||||
dns := &DNS{
|
|
||||||
QueryType: "A",
|
|
||||||
QueryName: "",
|
|
||||||
}
|
|
||||||
err := dns.validateAndSetDefault()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Should've returned an error because endpoint's dns didn't have a query name, which is a mandatory field for dns")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEndpoint_ValidateAndSetDefaultsWithInvalidDNSQueryType(t *testing.T) {
|
|
||||||
dns := &DNS{
|
|
||||||
QueryType: "B",
|
|
||||||
QueryName: "example.com",
|
|
||||||
}
|
|
||||||
err := dns.validateAndSetDefault()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Should've returned an error because endpoint's dns query type is invalid, it needs to be a valid query name like A, AAAA, CNAME...")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewEndpointStatus(t *testing.T) {
|
|
||||||
endpoint := &Endpoint{Name: "name", Group: "group"}
|
|
||||||
status := NewEndpointStatus(endpoint.Group, endpoint.Name)
|
|
||||||
if status.Name != endpoint.Name {
|
|
||||||
t.Errorf("expected %s, got %s", endpoint.Name, status.Name)
|
|
||||||
}
|
|
||||||
if status.Group != endpoint.Group {
|
|
||||||
t.Errorf("expected %s, got %s", endpoint.Group, status.Group)
|
|
||||||
}
|
|
||||||
if status.Key != "group_name" {
|
|
||||||
t.Errorf("expected %s, got %s", "group_name", status.Key)
|
|
||||||
}
|
|
||||||
}
|
|
4
main.go
4
main.go
|
@ -80,8 +80,8 @@ func initializeStorage(cfg *config.Config) {
|
||||||
}
|
}
|
||||||
// Remove all EndpointStatus that represent endpoints which no longer exist in the configuration
|
// Remove all EndpointStatus that represent endpoints which no longer exist in the configuration
|
||||||
var keys []string
|
var keys []string
|
||||||
for _, endpoint := range cfg.Endpoints {
|
for _, ep := range cfg.Endpoints {
|
||||||
keys = append(keys, endpoint.Key())
|
keys = append(keys, ep.Key())
|
||||||
}
|
}
|
||||||
for _, externalEndpoint := range cfg.ExternalEndpoints {
|
for _, externalEndpoint := range cfg.ExternalEndpoints {
|
||||||
keys = append(keys, externalEndpoint.Key())
|
keys = append(keys, externalEndpoint.Key())
|
||||||
|
|
|
@ -3,7 +3,7 @@ package metrics
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
@ -50,24 +50,24 @@ func initializePrometheusMetrics() {
|
||||||
|
|
||||||
// PublishMetricsForEndpoint publishes metrics for the given endpoint and its result.
|
// PublishMetricsForEndpoint publishes metrics for the given endpoint 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 PublishMetricsForEndpoint(endpoint *core.Endpoint, result *core.Result) {
|
func PublishMetricsForEndpoint(ep *endpoint.Endpoint, result *endpoint.Result) {
|
||||||
if !initializedMetrics {
|
if !initializedMetrics {
|
||||||
initializePrometheusMetrics()
|
initializePrometheusMetrics()
|
||||||
initializedMetrics = true
|
initializedMetrics = true
|
||||||
}
|
}
|
||||||
endpointType := endpoint.Type()
|
endpointType := ep.Type()
|
||||||
resultTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), strconv.FormatBool(result.Success)).Inc()
|
resultTotal.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType), strconv.FormatBool(result.Success)).Inc()
|
||||||
resultDurationSeconds.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Set(result.Duration.Seconds())
|
resultDurationSeconds.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType)).Set(result.Duration.Seconds())
|
||||||
if result.Connected {
|
if result.Connected {
|
||||||
resultConnectedTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Inc()
|
resultConnectedTotal.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType)).Inc()
|
||||||
}
|
}
|
||||||
if result.DNSRCode != "" {
|
if result.DNSRCode != "" {
|
||||||
resultCodeTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), result.DNSRCode).Inc()
|
resultCodeTotal.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType), result.DNSRCode).Inc()
|
||||||
}
|
}
|
||||||
if result.HTTPStatus != 0 {
|
if result.HTTPStatus != 0 {
|
||||||
resultCodeTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), strconv.Itoa(result.HTTPStatus)).Inc()
|
resultCodeTotal.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType), strconv.Itoa(result.HTTPStatus)).Inc()
|
||||||
}
|
}
|
||||||
if result.CertificateExpiration != 0 {
|
if result.CertificateExpiration != 0 {
|
||||||
resultCertificateExpirationSeconds.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Set(result.CertificateExpiration.Seconds())
|
resultCertificateExpirationSeconds.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType)).Set(result.CertificateExpiration.Seconds())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,19 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPublishMetricsForEndpoint(t *testing.T) {
|
func TestPublishMetricsForEndpoint(t *testing.T) {
|
||||||
httpEndpoint := &core.Endpoint{Name: "http-ep-name", Group: "http-ep-group", URL: "https://example.org"}
|
httpEndpoint := &endpoint.Endpoint{Name: "http-ep-name", Group: "http-ep-group", URL: "https://example.org"}
|
||||||
PublishMetricsForEndpoint(httpEndpoint, &core.Result{
|
PublishMetricsForEndpoint(httpEndpoint, &endpoint.Result{
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Duration: 123 * time.Millisecond,
|
Duration: 123 * time.Millisecond,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[STATUS] == 200", Success: true},
|
{Condition: "[STATUS] == 200", Success: true},
|
||||||
{Condition: "[CERTIFICATE_EXPIRATION] > 48h", Success: true},
|
{Condition: "[CERTIFICATE_EXPIRATION] > 48h", Success: true},
|
||||||
},
|
},
|
||||||
|
@ -43,11 +44,11 @@ gatus_results_total{group="http-ep-group",key="http-ep-group_http-ep-name",name=
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Expected no errors but got: %v", err)
|
t.Errorf("Expected no errors but got: %v", err)
|
||||||
}
|
}
|
||||||
PublishMetricsForEndpoint(httpEndpoint, &core.Result{
|
PublishMetricsForEndpoint(httpEndpoint, &endpoint.Result{
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Duration: 125 * time.Millisecond,
|
Duration: 125 * time.Millisecond,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[STATUS] == 200", Success: true},
|
{Condition: "[STATUS] == 200", Success: true},
|
||||||
{Condition: "[CERTIFICATE_EXPIRATION] > 47h", Success: false},
|
{Condition: "[CERTIFICATE_EXPIRATION] > 47h", Success: false},
|
||||||
},
|
},
|
||||||
|
@ -75,15 +76,15 @@ gatus_results_total{group="http-ep-group",key="http-ep-group_http-ep-name",name=
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Expected no errors but got: %v", err)
|
t.Errorf("Expected no errors but got: %v", err)
|
||||||
}
|
}
|
||||||
dnsEndpoint := &core.Endpoint{Name: "dns-ep-name", Group: "dns-ep-group", URL: "8.8.8.8", DNS: &core.DNS{
|
dnsEndpoint := &endpoint.Endpoint{Name: "dns-ep-name", Group: "dns-ep-group", URL: "8.8.8.8", DNSConfig: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com.",
|
QueryName: "example.com.",
|
||||||
}}
|
}}
|
||||||
PublishMetricsForEndpoint(dnsEndpoint, &core.Result{
|
PublishMetricsForEndpoint(dnsEndpoint, &endpoint.Result{
|
||||||
DNSRCode: "NOERROR",
|
DNSRCode: "NOERROR",
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Duration: 50 * time.Millisecond,
|
Duration: 50 * time.Millisecond,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[DNS_RCODE] == NOERROR", Success: true},
|
{Condition: "[DNS_RCODE] == NOERROR", Success: true},
|
||||||
},
|
},
|
||||||
Success: true,
|
Success: true,
|
||||||
|
|
|
@ -5,10 +5,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
|
||||||
"github.com/TwiN/gocache/v2"
|
"github.com/TwiN/gocache/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,13 +29,13 @@ func NewStore() (*Store, error) {
|
||||||
return store, nil
|
return store, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllEndpointStatuses returns all monitored core.EndpointStatus
|
// GetAllEndpointStatuses returns all monitored endpoint.Status
|
||||||
// with a subset of core.Result defined by the page and pageSize parameters
|
// with a subset of endpoint.Result defined by the page and pageSize parameters
|
||||||
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*core.EndpointStatus, error) {
|
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error) {
|
||||||
endpointStatuses := s.cache.GetAll()
|
endpointStatuses := s.cache.GetAll()
|
||||||
pagedEndpointStatuses := make([]*core.EndpointStatus, 0, len(endpointStatuses))
|
pagedEndpointStatuses := make([]*endpoint.Status, 0, len(endpointStatuses))
|
||||||
for _, v := range endpointStatuses {
|
for _, v := range endpointStatuses {
|
||||||
pagedEndpointStatuses = append(pagedEndpointStatuses, ShallowCopyEndpointStatus(v.(*core.EndpointStatus), params))
|
pagedEndpointStatuses = append(pagedEndpointStatuses, ShallowCopyEndpointStatus(v.(*endpoint.Status), params))
|
||||||
}
|
}
|
||||||
sort.Slice(pagedEndpointStatuses, func(i, j int) bool {
|
sort.Slice(pagedEndpointStatuses, func(i, j int) bool {
|
||||||
return pagedEndpointStatuses[i].Key < pagedEndpointStatuses[j].Key
|
return pagedEndpointStatuses[i].Key < pagedEndpointStatuses[j].Key
|
||||||
|
@ -45,17 +44,17 @@ func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEndpointStatus returns the endpoint status for a given endpoint name in the given group
|
// GetEndpointStatus returns the endpoint status for a given endpoint name in the given group
|
||||||
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) {
|
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
|
||||||
return s.GetEndpointStatusByKey(util.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
|
return s.GetEndpointStatusByKey(endpoint.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEndpointStatusByKey returns the endpoint status for a given key
|
// GetEndpointStatusByKey returns the endpoint status for a given key
|
||||||
func (s *Store) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) {
|
func (s *Store) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
|
||||||
endpointStatus := s.cache.GetValue(key)
|
endpointStatus := s.cache.GetValue(key)
|
||||||
if endpointStatus == nil {
|
if endpointStatus == nil {
|
||||||
return nil, common.ErrEndpointNotFound
|
return nil, common.ErrEndpointNotFound
|
||||||
}
|
}
|
||||||
return ShallowCopyEndpointStatus(endpointStatus.(*core.EndpointStatus), params), nil
|
return ShallowCopyEndpointStatus(endpointStatus.(*endpoint.Status), params), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUptimeByKey returns the uptime percentage during a time range
|
// GetUptimeByKey returns the uptime percentage during a time range
|
||||||
|
@ -64,7 +63,7 @@ func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
||||||
return 0, common.ErrInvalidTimeRange
|
return 0, common.ErrInvalidTimeRange
|
||||||
}
|
}
|
||||||
endpointStatus := s.cache.GetValue(key)
|
endpointStatus := s.cache.GetValue(key)
|
||||||
if endpointStatus == nil || endpointStatus.(*core.EndpointStatus).Uptime == nil {
|
if endpointStatus == nil || endpointStatus.(*endpoint.Status).Uptime == nil {
|
||||||
return 0, common.ErrEndpointNotFound
|
return 0, common.ErrEndpointNotFound
|
||||||
}
|
}
|
||||||
successfulExecutions := uint64(0)
|
successfulExecutions := uint64(0)
|
||||||
|
@ -72,7 +71,7 @@ func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
||||||
current := from
|
current := from
|
||||||
for to.Sub(current) >= 0 {
|
for to.Sub(current) >= 0 {
|
||||||
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
|
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
|
||||||
hourlyStats := endpointStatus.(*core.EndpointStatus).Uptime.HourlyStatistics[hourlyUnixTimestamp]
|
hourlyStats := endpointStatus.(*endpoint.Status).Uptime.HourlyStatistics[hourlyUnixTimestamp]
|
||||||
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
|
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
|
||||||
current = current.Add(time.Hour)
|
current = current.Add(time.Hour)
|
||||||
continue
|
continue
|
||||||
|
@ -93,14 +92,14 @@ func (s *Store) GetAverageResponseTimeByKey(key string, from, to time.Time) (int
|
||||||
return 0, common.ErrInvalidTimeRange
|
return 0, common.ErrInvalidTimeRange
|
||||||
}
|
}
|
||||||
endpointStatus := s.cache.GetValue(key)
|
endpointStatus := s.cache.GetValue(key)
|
||||||
if endpointStatus == nil || endpointStatus.(*core.EndpointStatus).Uptime == nil {
|
if endpointStatus == nil || endpointStatus.(*endpoint.Status).Uptime == nil {
|
||||||
return 0, common.ErrEndpointNotFound
|
return 0, common.ErrEndpointNotFound
|
||||||
}
|
}
|
||||||
current := from
|
current := from
|
||||||
var totalExecutions, totalResponseTime uint64
|
var totalExecutions, totalResponseTime uint64
|
||||||
for to.Sub(current) >= 0 {
|
for to.Sub(current) >= 0 {
|
||||||
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
|
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
|
||||||
hourlyStats := endpointStatus.(*core.EndpointStatus).Uptime.HourlyStatistics[hourlyUnixTimestamp]
|
hourlyStats := endpointStatus.(*endpoint.Status).Uptime.HourlyStatistics[hourlyUnixTimestamp]
|
||||||
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
|
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
|
||||||
current = current.Add(time.Hour)
|
current = current.Add(time.Hour)
|
||||||
continue
|
continue
|
||||||
|
@ -121,14 +120,14 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time
|
||||||
return nil, common.ErrInvalidTimeRange
|
return nil, common.ErrInvalidTimeRange
|
||||||
}
|
}
|
||||||
endpointStatus := s.cache.GetValue(key)
|
endpointStatus := s.cache.GetValue(key)
|
||||||
if endpointStatus == nil || endpointStatus.(*core.EndpointStatus).Uptime == nil {
|
if endpointStatus == nil || endpointStatus.(*endpoint.Status).Uptime == nil {
|
||||||
return nil, common.ErrEndpointNotFound
|
return nil, common.ErrEndpointNotFound
|
||||||
}
|
}
|
||||||
hourlyAverageResponseTimes := make(map[int64]int)
|
hourlyAverageResponseTimes := make(map[int64]int)
|
||||||
current := from
|
current := from
|
||||||
for to.Sub(current) >= 0 {
|
for to.Sub(current) >= 0 {
|
||||||
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
|
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
|
||||||
hourlyStats := endpointStatus.(*core.EndpointStatus).Uptime.HourlyStatistics[hourlyUnixTimestamp]
|
hourlyStats := endpointStatus.(*endpoint.Status).Uptime.HourlyStatistics[hourlyUnixTimestamp]
|
||||||
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
|
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
|
||||||
current = current.Add(time.Hour)
|
current = current.Add(time.Hour)
|
||||||
continue
|
continue
|
||||||
|
@ -140,24 +139,24 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert adds the observed result for the specified endpoint into the store
|
// Insert adds the observed result for the specified endpoint into the store
|
||||||
func (s *Store) Insert(endpoint *core.Endpoint, result *core.Result) error {
|
func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error {
|
||||||
key := endpoint.Key()
|
key := ep.Key()
|
||||||
s.Lock()
|
s.Lock()
|
||||||
status, exists := s.cache.Get(key)
|
status, exists := s.cache.Get(key)
|
||||||
if !exists {
|
if !exists {
|
||||||
status = core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
status = endpoint.NewStatus(ep.Group, ep.Name)
|
||||||
status.(*core.EndpointStatus).Events = append(status.(*core.EndpointStatus).Events, &core.Event{
|
status.(*endpoint.Status).Events = append(status.(*endpoint.Status).Events, &endpoint.Event{
|
||||||
Type: core.EventStart,
|
Type: endpoint.EventStart,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
AddResult(status.(*core.EndpointStatus), result)
|
AddResult(status.(*endpoint.Status), result)
|
||||||
s.cache.Set(key, status)
|
s.cache.Set(key, status)
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAllEndpointStatusesNotInKeys removes all EndpointStatus that are not within the keys provided
|
// DeleteAllEndpointStatusesNotInKeys removes all Status that are not within the keys provided
|
||||||
func (s *Store) DeleteAllEndpointStatusesNotInKeys(keys []string) int {
|
func (s *Store) DeleteAllEndpointStatusesNotInKeys(keys []string) int {
|
||||||
var keysToDelete []string
|
var keysToDelete []string
|
||||||
for _, existingKey := range s.cache.GetKeysByPattern("*", 0) {
|
for _, existingKey := range s.cache.GetKeysByPattern("*", 0) {
|
||||||
|
|
|
@ -4,30 +4,30 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
firstCondition = core.Condition("[STATUS] == 200")
|
firstCondition = endpoint.Condition("[STATUS] == 200")
|
||||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
|
||||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||||
|
|
||||||
now = time.Now()
|
now = time.Now()
|
||||||
|
|
||||||
testEndpoint = core.Endpoint{
|
testEndpoint = endpoint.Endpoint{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Group: "group",
|
Group: "group",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
}
|
}
|
||||||
testSuccessfulResult = core.Result{
|
testSuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
|
@ -37,7 +37,7 @@ var (
|
||||||
Timestamp: now,
|
Timestamp: now,
|
||||||
Duration: 150 * time.Millisecond,
|
Duration: 150 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
@ -52,7 +52,7 @@ var (
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testUnsuccessfulResult = core.Result{
|
testUnsuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
|
@ -62,7 +62,7 @@ var (
|
||||||
Timestamp: now,
|
Timestamp: now,
|
||||||
Duration: 750 * time.Millisecond,
|
Duration: 750 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
|
|
@ -3,7 +3,7 @@ package memory
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -13,14 +13,14 @@ const (
|
||||||
|
|
||||||
// processUptimeAfterResult processes the result by extracting the relevant from the result and recalculating the uptime
|
// processUptimeAfterResult processes the result by extracting the relevant from the result and recalculating the uptime
|
||||||
// if necessary
|
// if necessary
|
||||||
func processUptimeAfterResult(uptime *core.Uptime, result *core.Result) {
|
func processUptimeAfterResult(uptime *endpoint.Uptime, result *endpoint.Result) {
|
||||||
if uptime.HourlyStatistics == nil {
|
if uptime.HourlyStatistics == nil {
|
||||||
uptime.HourlyStatistics = make(map[int64]*core.HourlyUptimeStatistics)
|
uptime.HourlyStatistics = make(map[int64]*endpoint.HourlyUptimeStatistics)
|
||||||
}
|
}
|
||||||
unixTimestampFlooredAtHour := result.Timestamp.Truncate(time.Hour).Unix()
|
unixTimestampFlooredAtHour := result.Timestamp.Truncate(time.Hour).Unix()
|
||||||
hourlyStats, _ := uptime.HourlyStatistics[unixTimestampFlooredAtHour]
|
hourlyStats, _ := uptime.HourlyStatistics[unixTimestampFlooredAtHour]
|
||||||
if hourlyStats == nil {
|
if hourlyStats == nil {
|
||||||
hourlyStats = &core.HourlyUptimeStatistics{}
|
hourlyStats = &endpoint.HourlyUptimeStatistics{}
|
||||||
uptime.HourlyStatistics[unixTimestampFlooredAtHour] = hourlyStats
|
uptime.HourlyStatistics[unixTimestampFlooredAtHour] = hourlyStats
|
||||||
}
|
}
|
||||||
if result.Success {
|
if result.Success {
|
||||||
|
|
|
@ -4,17 +4,17 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkProcessUptimeAfterResult(b *testing.B) {
|
func BenchmarkProcessUptimeAfterResult(b *testing.B) {
|
||||||
uptime := core.NewUptime()
|
uptime := endpoint.NewUptime()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
||||||
// Start 12000 days ago
|
// Start 12000 days ago
|
||||||
timestamp := now.Add(-12000 * 24 * time.Hour)
|
timestamp := now.Add(-12000 * 24 * time.Hour)
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
processUptimeAfterResult(uptime, &core.Result{
|
processUptimeAfterResult(uptime, &endpoint.Result{
|
||||||
Duration: 18 * time.Millisecond,
|
Duration: 18 * time.Millisecond,
|
||||||
Success: n%15 == 0,
|
Success: n%15 == 0,
|
||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
|
|
|
@ -4,53 +4,53 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProcessUptimeAfterResult(t *testing.T) {
|
func TestProcessUptimeAfterResult(t *testing.T) {
|
||||||
endpoint := &core.Endpoint{Name: "name", Group: "group"}
|
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||||
status := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
status := endpoint.NewStatus(ep.Group, ep.Name)
|
||||||
uptime := status.Uptime
|
uptime := status.Uptime
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-7 * 24 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-7 * 24 * time.Hour), Success: true})
|
||||||
|
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-6 * 24 * time.Hour), Success: false})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-6 * 24 * time.Hour), Success: false})
|
||||||
|
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-8 * 24 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-8 * 24 * time.Hour), Success: true})
|
||||||
|
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-24 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-24 * time.Hour), Success: true})
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-12 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-12 * time.Hour), Success: true})
|
||||||
|
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-1 * time.Hour), Success: true, Duration: 10 * time.Millisecond})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-1 * time.Hour), Success: true, Duration: 10 * time.Millisecond})
|
||||||
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 10, 1, 1)
|
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 10, 1, 1)
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-30 * time.Minute), Success: false, Duration: 500 * time.Millisecond})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-30 * time.Minute), Success: false, Duration: 500 * time.Millisecond})
|
||||||
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 510, 2, 1)
|
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 510, 2, 1)
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-15 * time.Minute), Success: false, Duration: 25 * time.Millisecond})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-15 * time.Minute), Success: false, Duration: 25 * time.Millisecond})
|
||||||
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 535, 3, 1)
|
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 535, 3, 1)
|
||||||
|
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-10 * time.Minute), Success: false})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-10 * time.Minute), Success: false})
|
||||||
|
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-120 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-120 * time.Hour), Success: true})
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-119 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-119 * time.Hour), Success: true})
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-118 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-118 * time.Hour), Success: true})
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-117 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-117 * time.Hour), Success: true})
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-10 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-10 * time.Hour), Success: true})
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-30 * time.Minute), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-30 * time.Minute), Success: true})
|
||||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-25 * time.Minute), Success: true})
|
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-25 * time.Minute), Success: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
|
func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
|
||||||
endpoint := &core.Endpoint{Name: "name", Group: "group"}
|
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||||
status := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
status := endpoint.NewStatus(ep.Group, ep.Name)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
||||||
// Start 12 days ago
|
// Start 12 days ago
|
||||||
timestamp := now.Add(-12 * 24 * time.Hour)
|
timestamp := now.Add(-12 * 24 * time.Hour)
|
||||||
for timestamp.Unix() <= now.Unix() {
|
for timestamp.Unix() <= now.Unix() {
|
||||||
AddResult(status, &core.Result{Timestamp: timestamp, Success: true})
|
AddResult(status, &endpoint.Result{Timestamp: timestamp, Success: true})
|
||||||
if len(status.Uptime.HourlyStatistics) > numberOfHoursInTenDays {
|
if len(status.Uptime.HourlyStatistics) > numberOfHoursInTenDays {
|
||||||
t.Errorf("At no point in time should there be more than %d entries in status.SuccessfulExecutionsPerHour, but there are %d", numberOfHoursInTenDays, len(status.Uptime.HourlyStatistics))
|
t.Errorf("At no point in time should there be more than %d entries in status.SuccessfulExecutionsPerHour, but there are %d", numberOfHoursInTenDays, len(status.Uptime.HourlyStatistics))
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkHourlyStatistics(t *testing.T, hourlyUptimeStatistics *core.HourlyUptimeStatistics, expectedTotalExecutionsResponseTime uint64, expectedTotalExecutions uint64, expectedSuccessfulExecutions uint64) {
|
func checkHourlyStatistics(t *testing.T, hourlyUptimeStatistics *endpoint.HourlyUptimeStatistics, expectedTotalExecutionsResponseTime uint64, expectedTotalExecutions uint64, expectedSuccessfulExecutions uint64) {
|
||||||
if hourlyUptimeStatistics.TotalExecutionsResponseTime != expectedTotalExecutionsResponseTime {
|
if hourlyUptimeStatistics.TotalExecutionsResponseTime != expectedTotalExecutionsResponseTime {
|
||||||
t.Error("TotalExecutionsResponseTime should've been", expectedTotalExecutionsResponseTime, "got", hourlyUptimeStatistics.TotalExecutionsResponseTime)
|
t.Error("TotalExecutionsResponseTime should've been", expectedTotalExecutionsResponseTime, "got", hourlyUptimeStatistics.TotalExecutionsResponseTime)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
package memory
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ShallowCopyEndpointStatus returns a shallow copy of a EndpointStatus with only the results
|
// ShallowCopyEndpointStatus returns a shallow copy of a Status with only the results
|
||||||
// within the range defined by the page and pageSize parameters
|
// within the range defined by the page and pageSize parameters
|
||||||
func ShallowCopyEndpointStatus(ss *core.EndpointStatus, params *paging.EndpointStatusParams) *core.EndpointStatus {
|
func ShallowCopyEndpointStatus(ss *endpoint.Status, params *paging.EndpointStatusParams) *endpoint.Status {
|
||||||
shallowCopy := &core.EndpointStatus{
|
shallowCopy := &endpoint.Status{
|
||||||
Name: ss.Name,
|
Name: ss.Name,
|
||||||
Group: ss.Group,
|
Group: ss.Group,
|
||||||
Key: ss.Key,
|
Key: ss.Key,
|
||||||
Uptime: core.NewUptime(),
|
Uptime: endpoint.NewUptime(),
|
||||||
}
|
}
|
||||||
numberOfResults := len(ss.Results)
|
numberOfResults := len(ss.Results)
|
||||||
resultsStart, resultsEnd := getStartAndEndIndex(numberOfResults, params.ResultsPage, params.ResultsPageSize)
|
resultsStart, resultsEnd := getStartAndEndIndex(numberOfResults, params.ResultsPage, params.ResultsPageSize)
|
||||||
if resultsStart < 0 || resultsEnd < 0 {
|
if resultsStart < 0 || resultsEnd < 0 {
|
||||||
shallowCopy.Results = []*core.Result{}
|
shallowCopy.Results = []*endpoint.Result{}
|
||||||
} else {
|
} else {
|
||||||
shallowCopy.Results = ss.Results[resultsStart:resultsEnd]
|
shallowCopy.Results = ss.Results[resultsStart:resultsEnd]
|
||||||
}
|
}
|
||||||
numberOfEvents := len(ss.Events)
|
numberOfEvents := len(ss.Events)
|
||||||
eventsStart, eventsEnd := getStartAndEndIndex(numberOfEvents, params.EventsPage, params.EventsPageSize)
|
eventsStart, eventsEnd := getStartAndEndIndex(numberOfEvents, params.EventsPage, params.EventsPageSize)
|
||||||
if eventsStart < 0 || eventsEnd < 0 {
|
if eventsStart < 0 || eventsEnd < 0 {
|
||||||
shallowCopy.Events = []*core.Event{}
|
shallowCopy.Events = []*endpoint.Event{}
|
||||||
} else {
|
} else {
|
||||||
shallowCopy.Events = ss.Events[eventsStart:eventsEnd]
|
shallowCopy.Events = ss.Events[eventsStart:eventsEnd]
|
||||||
}
|
}
|
||||||
|
@ -49,16 +49,16 @@ func getStartAndEndIndex(numberOfResults int, page, pageSize int) (int, int) {
|
||||||
return start, end
|
return start, end
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddResult adds a Result to EndpointStatus.Results and makes sure that there are
|
// AddResult adds a Result to Status.Results and makes sure that there are
|
||||||
// no more than MaximumNumberOfResults results in the Results slice
|
// no more than MaximumNumberOfResults results in the Results slice
|
||||||
func AddResult(ss *core.EndpointStatus, result *core.Result) {
|
func AddResult(ss *endpoint.Status, result *endpoint.Result) {
|
||||||
if ss == nil {
|
if ss == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(ss.Results) > 0 {
|
if len(ss.Results) > 0 {
|
||||||
// Check if there's any change since the last result
|
// Check if there's any change since the last result
|
||||||
if ss.Results[len(ss.Results)-1].Success != result.Success {
|
if ss.Results[len(ss.Results)-1].Success != result.Success {
|
||||||
ss.Events = append(ss.Events, core.NewEventFromResult(result))
|
ss.Events = append(ss.Events, endpoint.NewEventFromResult(result))
|
||||||
if len(ss.Events) > common.MaximumNumberOfEvents {
|
if len(ss.Events) > common.MaximumNumberOfEvents {
|
||||||
// Doing ss.Events[1:] would usually be sufficient, but in the case where for some reason, the slice has
|
// Doing ss.Events[1:] would usually be sufficient, but in the case where for some reason, the slice has
|
||||||
// more than one extra element, we can get rid of all of them at once and thus returning the slice to a
|
// more than one extra element, we can get rid of all of them at once and thus returning the slice to a
|
||||||
|
@ -68,7 +68,7 @@ func AddResult(ss *core.EndpointStatus, result *core.Result) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This is the first result, so we need to add the first healthy/unhealthy event
|
// This is the first result, so we need to add the first healthy/unhealthy event
|
||||||
ss.Events = append(ss.Events, core.NewEventFromResult(result))
|
ss.Events = append(ss.Events, endpoint.NewEventFromResult(result))
|
||||||
}
|
}
|
||||||
ss.Results = append(ss.Results, result)
|
ss.Results = append(ss.Results, result)
|
||||||
if len(ss.Results) > common.MaximumNumberOfResults {
|
if len(ss.Results) > common.MaximumNumberOfResults {
|
||||||
|
|
|
@ -3,14 +3,14 @@ package memory
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkShallowCopyEndpointStatus(b *testing.B) {
|
func BenchmarkShallowCopyEndpointStatus(b *testing.B) {
|
||||||
endpoint := &testEndpoint
|
ep := &testEndpoint
|
||||||
status := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
status := endpoint.NewStatus(ep.Group, ep.Name)
|
||||||
for i := 0; i < common.MaximumNumberOfResults; i++ {
|
for i := 0; i < common.MaximumNumberOfResults; i++ {
|
||||||
AddResult(status, &testSuccessfulResult)
|
AddResult(status, &testSuccessfulResult)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,16 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAddResult(t *testing.T) {
|
func TestAddResult(t *testing.T) {
|
||||||
endpoint := &core.Endpoint{Name: "name", Group: "group"}
|
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||||
endpointStatus := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
|
||||||
for i := 0; i < (common.MaximumNumberOfResults+common.MaximumNumberOfEvents)*2; i++ {
|
for i := 0; i < (common.MaximumNumberOfResults+common.MaximumNumberOfEvents)*2; i++ {
|
||||||
AddResult(endpointStatus, &core.Result{Success: i%2 == 0, Timestamp: time.Now()})
|
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: time.Now()})
|
||||||
}
|
}
|
||||||
if len(endpointStatus.Results) != common.MaximumNumberOfResults {
|
if len(endpointStatus.Results) != common.MaximumNumberOfResults {
|
||||||
t.Errorf("expected endpointStatus.Results to not exceed a length of %d", common.MaximumNumberOfResults)
|
t.Errorf("expected endpointStatus.Results to not exceed a length of %d", common.MaximumNumberOfResults)
|
||||||
|
@ -22,15 +22,15 @@ func TestAddResult(t *testing.T) {
|
||||||
t.Errorf("expected endpointStatus.Events to not exceed a length of %d", common.MaximumNumberOfEvents)
|
t.Errorf("expected endpointStatus.Events to not exceed a length of %d", common.MaximumNumberOfEvents)
|
||||||
}
|
}
|
||||||
// Try to add nil endpointStatus
|
// Try to add nil endpointStatus
|
||||||
AddResult(nil, &core.Result{Timestamp: time.Now()})
|
AddResult(nil, &endpoint.Result{Timestamp: time.Now()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShallowCopyEndpointStatus(t *testing.T) {
|
func TestShallowCopyEndpointStatus(t *testing.T) {
|
||||||
endpoint := &core.Endpoint{Name: "name", Group: "group"}
|
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||||
endpointStatus := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
|
||||||
ts := time.Now().Add(-25 * time.Hour)
|
ts := time.Now().Add(-25 * time.Hour)
|
||||||
for i := 0; i < 25; i++ {
|
for i := 0; i < 25; i++ {
|
||||||
AddResult(endpointStatus, &core.Result{Success: i%2 == 0, Timestamp: ts})
|
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: ts})
|
||||||
ts = ts.Add(time.Hour)
|
ts = ts.Add(time.Hour)
|
||||||
}
|
}
|
||||||
if len(ShallowCopyEndpointStatus(endpointStatus, paging.NewEndpointStatusParams().WithResults(-1, -1)).Results) != 0 {
|
if len(ShallowCopyEndpointStatus(endpointStatus, paging.NewEndpointStatusParams().WithResults(-1, -1)).Results) != 0 {
|
||||||
|
|
|
@ -9,10 +9,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
|
||||||
"github.com/TwiN/gocache/v2"
|
"github.com/TwiN/gocache/v2"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
|
@ -100,9 +99,9 @@ func (s *Store) createSchema() error {
|
||||||
return s.createPostgresSchema()
|
return s.createPostgresSchema()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllEndpointStatuses returns all monitored core.EndpointStatus
|
// GetAllEndpointStatuses returns all monitored endpoint.Status
|
||||||
// with a subset of core.Result defined by the page and pageSize parameters
|
// with a subset of endpoint.Result defined by the page and pageSize parameters
|
||||||
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*core.EndpointStatus, error) {
|
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error) {
|
||||||
tx, err := s.db.Begin()
|
tx, err := s.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -112,7 +111,7 @@ func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
endpointStatuses := make([]*core.EndpointStatus, 0, len(keys))
|
endpointStatuses := make([]*endpoint.Status, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
endpointStatus, err := s.getEndpointStatusByKey(tx, key, params)
|
endpointStatus, err := s.getEndpointStatusByKey(tx, key, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -127,12 +126,12 @@ func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEndpointStatus returns the endpoint status for a given endpoint name in the given group
|
// GetEndpointStatus returns the endpoint status for a given endpoint name in the given group
|
||||||
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) {
|
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
|
||||||
return s.GetEndpointStatusByKey(util.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
|
return s.GetEndpointStatusByKey(endpoint.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEndpointStatusByKey returns the endpoint status for a given key
|
// GetEndpointStatusByKey returns the endpoint status for a given key
|
||||||
func (s *Store) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) {
|
func (s *Store) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
|
||||||
tx, err := s.db.Begin()
|
tx, err := s.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -224,30 +223,30 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert adds the observed result for the specified endpoint into the store
|
// Insert adds the observed result for the specified endpoint into the store
|
||||||
func (s *Store) Insert(endpoint *core.Endpoint, result *core.Result) error {
|
func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error {
|
||||||
tx, err := s.db.Begin()
|
tx, err := s.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
endpointID, err := s.getEndpointID(tx, endpoint)
|
endpointID, err := s.getEndpointID(tx, ep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrEndpointNotFound) {
|
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||||
// Endpoint doesn't exist in the database, insert it
|
// Endpoint doesn't exist in the database, insert it
|
||||||
if endpointID, err = s.insertEndpoint(tx, endpoint); err != nil {
|
if endpointID, err = s.insertEndpoint(tx, ep); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
log.Printf("[sql.Insert] Failed to create endpoint with group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to create endpoint with group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
log.Printf("[sql.Insert] Failed to retrieve id of endpoint with group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to retrieve id of endpoint with group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// First, we need to check if we need to insert a new event.
|
// First, we need to check if we need to insert a new event.
|
||||||
//
|
//
|
||||||
// A new event must be added if either of the following cases happen:
|
// A new event must be added if either of the following cases happen:
|
||||||
// 1. There is only 1 event. The total number of events for a endpoint can only be 1 if the only existing event is
|
// 1. There is only 1 event. The total number of events for an endpoint can only be 1 if the only existing event is
|
||||||
// of type EventStart, in which case we will have to create a new event of type EventHealthy or EventUnhealthy
|
// of type EventStart, in which case we will have to create a new event of type EventHealthy or EventUnhealthy
|
||||||
// based on result.Success.
|
// based on result.Success.
|
||||||
// 2. The lastResult.Success != result.Success. This implies that the endpoint went from healthy to unhealthy or
|
// 2. The lastResult.Success != result.Success. This implies that the endpoint went from healthy to unhealthy or
|
||||||
|
@ -256,38 +255,38 @@ func (s *Store) Insert(endpoint *core.Endpoint, result *core.Result) error {
|
||||||
numberOfEvents, err := s.getNumberOfEventsByEndpointID(tx, endpointID)
|
numberOfEvents, err := s.getNumberOfEventsByEndpointID(tx, endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Silently fail
|
// Silently fail
|
||||||
log.Printf("[sql.Insert] Failed to retrieve total number of events for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to retrieve total number of events for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
}
|
}
|
||||||
if numberOfEvents == 0 {
|
if numberOfEvents == 0 {
|
||||||
// There's no events yet, which means we need to add the EventStart and the first healthy/unhealthy event
|
// There's no events yet, which means we need to add the EventStart and the first healthy/unhealthy event
|
||||||
err = s.insertEndpointEvent(tx, endpointID, &core.Event{
|
err = s.insertEndpointEvent(tx, endpointID, &endpoint.Event{
|
||||||
Type: core.EventStart,
|
Type: endpoint.EventStart,
|
||||||
Timestamp: result.Timestamp.Add(-50 * time.Millisecond),
|
Timestamp: result.Timestamp.Add(-50 * time.Millisecond),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Silently fail
|
// Silently fail
|
||||||
log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", core.EventStart, endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", endpoint.EventStart, ep.Group, ep.Name, err.Error())
|
||||||
}
|
}
|
||||||
event := core.NewEventFromResult(result)
|
event := endpoint.NewEventFromResult(result)
|
||||||
if err = s.insertEndpointEvent(tx, endpointID, event); err != nil {
|
if err = s.insertEndpointEvent(tx, endpointID, event); err != nil {
|
||||||
// Silently fail
|
// Silently fail
|
||||||
log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", event.Type, endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", event.Type, ep.Group, ep.Name, err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Get the success value of the previous result
|
// Get the success value of the previous result
|
||||||
var lastResultSuccess bool
|
var lastResultSuccess bool
|
||||||
if lastResultSuccess, err = s.getLastEndpointResultSuccessValue(tx, endpointID); err != nil {
|
if lastResultSuccess, err = s.getLastEndpointResultSuccessValue(tx, endpointID); err != nil {
|
||||||
log.Printf("[sql.Insert] Failed to retrieve outcome of previous result for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to retrieve outcome of previous result for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
} else {
|
} else {
|
||||||
// If we managed to retrieve the outcome of the previous result, we'll compare it with the new result.
|
// If we managed to retrieve the outcome of the previous result, we'll compare it with the new result.
|
||||||
// If the final outcome (success or failure) of the previous and the new result aren't the same, it means
|
// If the final outcome (success or failure) of the previous and the new result aren't the same, it means
|
||||||
// that the endpoint either went from Healthy to Unhealthy or Unhealthy -> Healthy, therefore, we'll add
|
// that the endpoint either went from Healthy to Unhealthy or Unhealthy -> Healthy, therefore, we'll add
|
||||||
// an event to mark the change in state
|
// an event to mark the change in state
|
||||||
if lastResultSuccess != result.Success {
|
if lastResultSuccess != result.Success {
|
||||||
event := core.NewEventFromResult(result)
|
event := endpoint.NewEventFromResult(result)
|
||||||
if err = s.insertEndpointEvent(tx, endpointID, event); err != nil {
|
if err = s.insertEndpointEvent(tx, endpointID, event); err != nil {
|
||||||
// Silently fail
|
// Silently fail
|
||||||
log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", event.Type, endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", event.Type, ep.Group, ep.Name, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -296,45 +295,45 @@ func (s *Store) Insert(endpoint *core.Endpoint, result *core.Result) error {
|
||||||
// (since we're only deleting MaximumNumberOfEvents at a time instead of 1)
|
// (since we're only deleting MaximumNumberOfEvents at a time instead of 1)
|
||||||
if numberOfEvents > eventsCleanUpThreshold {
|
if numberOfEvents > eventsCleanUpThreshold {
|
||||||
if err = s.deleteOldEndpointEvents(tx, endpointID); err != nil {
|
if err = s.deleteOldEndpointEvents(tx, endpointID); err != nil {
|
||||||
log.Printf("[sql.Insert] Failed to delete old events for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to delete old events for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Second, we need to insert the result.
|
// Second, we need to insert the result.
|
||||||
if err = s.insertEndpointResult(tx, endpointID, result); err != nil {
|
if err = s.insertEndpointResult(tx, endpointID, result); err != nil {
|
||||||
log.Printf("[sql.Insert] Failed to insert result for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to insert result for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
_ = tx.Rollback() // If we can't insert the result, we'll rollback now since there's no point continuing
|
_ = tx.Rollback() // If we can't insert the result, we'll rollback now since there's no point continuing
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Clean up old results
|
// Clean up old results
|
||||||
numberOfResults, err := s.getNumberOfResultsByEndpointID(tx, endpointID)
|
numberOfResults, err := s.getNumberOfResultsByEndpointID(tx, endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[sql.Insert] Failed to retrieve total number of results for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to retrieve total number of results for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
} else {
|
} else {
|
||||||
if numberOfResults > resultsCleanUpThreshold {
|
if numberOfResults > resultsCleanUpThreshold {
|
||||||
if err = s.deleteOldEndpointResults(tx, endpointID); err != nil {
|
if err = s.deleteOldEndpointResults(tx, endpointID); err != nil {
|
||||||
log.Printf("[sql.Insert] Failed to delete old results for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to delete old results for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Finally, we need to insert the uptime data.
|
// Finally, we need to insert the uptime data.
|
||||||
// Because the uptime data significantly outlives the results, we can't rely on the results for determining the uptime
|
// Because the uptime data significantly outlives the results, we can't rely on the results for determining the uptime
|
||||||
if err = s.updateEndpointUptime(tx, endpointID, result); err != nil {
|
if err = s.updateEndpointUptime(tx, endpointID, result); err != nil {
|
||||||
log.Printf("[sql.Insert] Failed to update uptime for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to update uptime for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
}
|
}
|
||||||
// Clean up old uptime entries
|
// Clean up old uptime entries
|
||||||
ageOfOldestUptimeEntry, err := s.getAgeOfOldestEndpointUptimeEntry(tx, endpointID)
|
ageOfOldestUptimeEntry, err := s.getAgeOfOldestEndpointUptimeEntry(tx, endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[sql.Insert] Failed to retrieve oldest endpoint uptime entry for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to retrieve oldest endpoint uptime entry for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
} else {
|
} else {
|
||||||
if ageOfOldestUptimeEntry > uptimeCleanUpThreshold {
|
if ageOfOldestUptimeEntry > uptimeCleanUpThreshold {
|
||||||
if err = s.deleteOldUptimeEntries(tx, endpointID, time.Now().Add(-(uptimeRetention + time.Hour))); err != nil {
|
if err = s.deleteOldUptimeEntries(tx, endpointID, time.Now().Add(-(uptimeRetention + time.Hour))); err != nil {
|
||||||
log.Printf("[sql.Insert] Failed to delete old uptime entries for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error())
|
log.Printf("[sql.Insert] Failed to delete old uptime entries for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.writeThroughCache != nil {
|
if s.writeThroughCache != nil {
|
||||||
cacheKeysToRefresh := s.writeThroughCache.GetKeysByPattern(endpoint.Key()+"*", 0)
|
cacheKeysToRefresh := s.writeThroughCache.GetKeysByPattern(ep.Key()+"*", 0)
|
||||||
for _, cacheKey := range cacheKeysToRefresh {
|
for _, cacheKey := range cacheKeysToRefresh {
|
||||||
s.writeThroughCache.Delete(cacheKey)
|
s.writeThroughCache.Delete(cacheKey)
|
||||||
endpointKey, params, err := extractKeyAndParamsFromCacheKey(cacheKey)
|
endpointKey, params, err := extractKeyAndParamsFromCacheKey(cacheKey)
|
||||||
|
@ -405,14 +404,14 @@ func (s *Store) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertEndpoint inserts an endpoint in the store and returns the generated id of said endpoint
|
// insertEndpoint inserts an endpoint in the store and returns the generated id of said endpoint
|
||||||
func (s *Store) insertEndpoint(tx *sql.Tx, endpoint *core.Endpoint) (int64, error) {
|
func (s *Store) insertEndpoint(tx *sql.Tx, ep *endpoint.Endpoint) (int64, error) {
|
||||||
//log.Printf("[sql.insertEndpoint] Inserting endpoint with group=%s and name=%s", endpoint.Group, endpoint.Name)
|
//log.Printf("[sql.insertEndpoint] Inserting endpoint with group=%s and name=%s", ep.Group, ep.Name)
|
||||||
var id int64
|
var id int64
|
||||||
err := tx.QueryRow(
|
err := tx.QueryRow(
|
||||||
"INSERT INTO endpoints (endpoint_key, endpoint_name, endpoint_group) VALUES ($1, $2, $3) RETURNING endpoint_id",
|
"INSERT INTO endpoints (endpoint_key, endpoint_name, endpoint_group) VALUES ($1, $2, $3) RETURNING endpoint_id",
|
||||||
endpoint.Key(),
|
ep.Key(),
|
||||||
endpoint.Name,
|
ep.Name,
|
||||||
endpoint.Group,
|
ep.Group,
|
||||||
).Scan(&id)
|
).Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
@ -421,7 +420,7 @@ func (s *Store) insertEndpoint(tx *sql.Tx, endpoint *core.Endpoint) (int64, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertEndpointEvent inserts en event in the store
|
// insertEndpointEvent inserts en event in the store
|
||||||
func (s *Store) insertEndpointEvent(tx *sql.Tx, endpointID int64, event *core.Event) error {
|
func (s *Store) insertEndpointEvent(tx *sql.Tx, endpointID int64, event *endpoint.Event) error {
|
||||||
_, err := tx.Exec(
|
_, err := tx.Exec(
|
||||||
"INSERT INTO endpoint_events (endpoint_id, event_type, event_timestamp) VALUES ($1, $2, $3)",
|
"INSERT INTO endpoint_events (endpoint_id, event_type, event_timestamp) VALUES ($1, $2, $3)",
|
||||||
endpointID,
|
endpointID,
|
||||||
|
@ -435,7 +434,7 @@ func (s *Store) insertEndpointEvent(tx *sql.Tx, endpointID int64, event *core.Ev
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertEndpointResult inserts a result in the store
|
// insertEndpointResult inserts a result in the store
|
||||||
func (s *Store) insertEndpointResult(tx *sql.Tx, endpointID int64, result *core.Result) error {
|
func (s *Store) insertEndpointResult(tx *sql.Tx, endpointID int64, result *endpoint.Result) error {
|
||||||
var endpointResultID int64
|
var endpointResultID int64
|
||||||
err := tx.QueryRow(
|
err := tx.QueryRow(
|
||||||
`
|
`
|
||||||
|
@ -462,7 +461,7 @@ func (s *Store) insertEndpointResult(tx *sql.Tx, endpointID int64, result *core.
|
||||||
return s.insertConditionResults(tx, endpointResultID, result.ConditionResults)
|
return s.insertConditionResults(tx, endpointResultID, result.ConditionResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) insertConditionResults(tx *sql.Tx, endpointResultID int64, conditionResults []*core.ConditionResult) error {
|
func (s *Store) insertConditionResults(tx *sql.Tx, endpointResultID int64, conditionResults []*endpoint.ConditionResult) error {
|
||||||
var err error
|
var err error
|
||||||
for _, cr := range conditionResults {
|
for _, cr := range conditionResults {
|
||||||
_, err = tx.Exec("INSERT INTO endpoint_result_conditions (endpoint_result_id, condition, success) VALUES ($1, $2, $3)",
|
_, err = tx.Exec("INSERT INTO endpoint_result_conditions (endpoint_result_id, condition, success) VALUES ($1, $2, $3)",
|
||||||
|
@ -477,7 +476,7 @@ func (s *Store) insertConditionResults(tx *sql.Tx, endpointResultID int64, condi
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) updateEndpointUptime(tx *sql.Tx, endpointID int64, result *core.Result) error {
|
func (s *Store) updateEndpointUptime(tx *sql.Tx, endpointID int64, result *endpoint.Result) error {
|
||||||
unixTimestampFlooredAtHour := result.Timestamp.Truncate(time.Hour).Unix()
|
unixTimestampFlooredAtHour := result.Timestamp.Truncate(time.Hour).Unix()
|
||||||
var successfulExecutions int
|
var successfulExecutions int
|
||||||
if result.Success {
|
if result.Success {
|
||||||
|
@ -514,12 +513,12 @@ func (s *Store) getAllEndpointKeys(tx *sql.Tx) (keys []string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) getEndpointStatusByKey(tx *sql.Tx, key string, parameters *paging.EndpointStatusParams) (*core.EndpointStatus, error) {
|
func (s *Store) getEndpointStatusByKey(tx *sql.Tx, key string, parameters *paging.EndpointStatusParams) (*endpoint.Status, error) {
|
||||||
var cacheKey string
|
var cacheKey string
|
||||||
if s.writeThroughCache != nil {
|
if s.writeThroughCache != nil {
|
||||||
cacheKey = generateCacheKey(key, parameters)
|
cacheKey = generateCacheKey(key, parameters)
|
||||||
if cachedEndpointStatus, exists := s.writeThroughCache.Get(cacheKey); exists {
|
if cachedEndpointStatus, exists := s.writeThroughCache.Get(cacheKey); exists {
|
||||||
if castedCachedEndpointStatus, ok := cachedEndpointStatus.(*core.EndpointStatus); ok {
|
if castedCachedEndpointStatus, ok := cachedEndpointStatus.(*endpoint.Status); ok {
|
||||||
return castedCachedEndpointStatus, nil
|
return castedCachedEndpointStatus, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -528,7 +527,7 @@ func (s *Store) getEndpointStatusByKey(tx *sql.Tx, key string, parameters *pagin
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
endpointStatus := core.NewEndpointStatus(group, endpointName)
|
endpointStatus := endpoint.NewStatus(group, endpointName)
|
||||||
if parameters.EventsPageSize > 0 {
|
if parameters.EventsPageSize > 0 {
|
||||||
if endpointStatus.Events, err = s.getEndpointEventsByEndpointID(tx, endpointID, parameters.EventsPage, parameters.EventsPageSize); err != nil {
|
if endpointStatus.Events, err = s.getEndpointEventsByEndpointID(tx, endpointID, parameters.EventsPage, parameters.EventsPageSize); err != nil {
|
||||||
log.Printf("[sql.getEndpointStatusByKey] Failed to retrieve events for key=%s: %s", key, err.Error())
|
log.Printf("[sql.getEndpointStatusByKey] Failed to retrieve events for key=%s: %s", key, err.Error())
|
||||||
|
@ -564,7 +563,7 @@ func (s *Store) getEndpointIDGroupAndNameByKey(tx *sql.Tx, key string) (id int64
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) getEndpointEventsByEndpointID(tx *sql.Tx, endpointID int64, page, pageSize int) (events []*core.Event, err error) {
|
func (s *Store) getEndpointEventsByEndpointID(tx *sql.Tx, endpointID int64, page, pageSize int) (events []*endpoint.Event, err error) {
|
||||||
rows, err := tx.Query(
|
rows, err := tx.Query(
|
||||||
`
|
`
|
||||||
SELECT event_type, event_timestamp
|
SELECT event_type, event_timestamp
|
||||||
|
@ -581,14 +580,14 @@ func (s *Store) getEndpointEventsByEndpointID(tx *sql.Tx, endpointID int64, page
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
event := &core.Event{}
|
event := &endpoint.Event{}
|
||||||
_ = rows.Scan(&event.Type, &event.Timestamp)
|
_ = rows.Scan(&event.Type, &event.Timestamp)
|
||||||
events = append(events, event)
|
events = append(events, event)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, page, pageSize int) (results []*core.Result, err error) {
|
func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, page, pageSize int) (results []*endpoint.Result, err error) {
|
||||||
rows, err := tx.Query(
|
rows, err := tx.Query(
|
||||||
`
|
`
|
||||||
SELECT endpoint_result_id, success, errors, connected, status, dns_rcode, certificate_expiration, domain_expiration, hostname, ip, duration, timestamp
|
SELECT endpoint_result_id, success, errors, connected, status, dns_rcode, certificate_expiration, domain_expiration, hostname, ip, duration, timestamp
|
||||||
|
@ -604,9 +603,9 @@ func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, pag
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
idResultMap := make(map[int64]*core.Result)
|
idResultMap := make(map[int64]*endpoint.Result)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
result := &core.Result{}
|
result := &endpoint.Result{}
|
||||||
var id int64
|
var id int64
|
||||||
var joinedErrors string
|
var joinedErrors string
|
||||||
err = rows.Scan(&id, &result.Success, &joinedErrors, &result.Connected, &result.HTTPStatus, &result.DNSRCode, &result.CertificateExpiration, &result.DomainExpiration, &result.Hostname, &result.IP, &result.Duration, &result.Timestamp)
|
err = rows.Scan(&id, &result.Success, &joinedErrors, &result.Connected, &result.HTTPStatus, &result.DNSRCode, &result.CertificateExpiration, &result.DomainExpiration, &result.Hostname, &result.IP, &result.Duration, &result.Timestamp)
|
||||||
|
@ -618,7 +617,7 @@ func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, pag
|
||||||
result.Errors = strings.Split(joinedErrors, arraySeparator)
|
result.Errors = strings.Split(joinedErrors, arraySeparator)
|
||||||
}
|
}
|
||||||
// This is faster than using a subselect
|
// This is faster than using a subselect
|
||||||
results = append([]*core.Result{result}, results...)
|
results = append([]*endpoint.Result{result}, results...)
|
||||||
idResultMap[id] = result
|
idResultMap[id] = result
|
||||||
}
|
}
|
||||||
if len(idResultMap) == 0 {
|
if len(idResultMap) == 0 {
|
||||||
|
@ -643,7 +642,7 @@ func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, pag
|
||||||
}
|
}
|
||||||
defer rows.Close() // explicitly defer the close in case an error happens during the scan
|
defer rows.Close() // explicitly defer the close in case an error happens during the scan
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
conditionResult := &core.ConditionResult{}
|
conditionResult := &endpoint.ConditionResult{}
|
||||||
var endpointResultID int64
|
var endpointResultID int64
|
||||||
if err = rows.Scan(&endpointResultID, &conditionResult.Condition, &conditionResult.Success); err != nil {
|
if err = rows.Scan(&endpointResultID, &conditionResult.Condition, &conditionResult.Success); err != nil {
|
||||||
return
|
return
|
||||||
|
@ -734,9 +733,9 @@ func (s *Store) getEndpointHourlyAverageResponseTimes(tx *sql.Tx, endpointID int
|
||||||
return hourlyAverageResponseTimes, nil
|
return hourlyAverageResponseTimes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) getEndpointID(tx *sql.Tx, endpoint *core.Endpoint) (int64, error) {
|
func (s *Store) getEndpointID(tx *sql.Tx, ep *endpoint.Endpoint) (int64, error) {
|
||||||
var id int64
|
var id int64
|
||||||
err := tx.QueryRow("SELECT endpoint_id FROM endpoints WHERE endpoint_key = $1", endpoint.Key()).Scan(&id)
|
err := tx.QueryRow("SELECT endpoint_id FROM endpoints WHERE endpoint_key = $1", ep.Key()).Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return 0, common.ErrEndpointNotFound
|
return 0, common.ErrEndpointNotFound
|
||||||
|
|
|
@ -4,31 +4,31 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
firstCondition = core.Condition("[STATUS] == 200")
|
firstCondition = endpoint.Condition("[STATUS] == 200")
|
||||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
|
||||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||||
|
|
||||||
now = time.Now()
|
now = time.Now()
|
||||||
|
|
||||||
testEndpoint = core.Endpoint{
|
testEndpoint = endpoint.Endpoint{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Group: "group",
|
Group: "group",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
}
|
}
|
||||||
testSuccessfulResult = core.Result{
|
testSuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
|
@ -38,7 +38,7 @@ var (
|
||||||
Timestamp: now,
|
Timestamp: now,
|
||||||
Duration: 150 * time.Millisecond,
|
Duration: 150 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
@ -53,7 +53,7 @@ var (
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testUnsuccessfulResult = core.Result{
|
testUnsuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
|
@ -63,7 +63,7 @@ var (
|
||||||
Timestamp: now,
|
Timestamp: now,
|
||||||
Duration: 750 * time.Millisecond,
|
Duration: 750 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
@ -100,7 +100,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
|
||||||
now := time.Now().Truncate(time.Hour)
|
now := time.Now().Truncate(time.Hour)
|
||||||
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
||||||
|
|
||||||
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-5 * time.Hour), Success: true})
|
store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-5 * time.Hour), Success: true})
|
||||||
|
|
||||||
tx, _ := store.db.Begin()
|
tx, _ := store.db.Begin()
|
||||||
oldest, _ := store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
oldest, _ := store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
||||||
|
@ -110,7 +110,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The oldest cache entry should remain at ~5 hours old, because this entry is more recent
|
// The oldest cache entry should remain at ~5 hours old, because this entry is more recent
|
||||||
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-3 * time.Hour), Success: true})
|
store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-3 * time.Hour), Success: true})
|
||||||
|
|
||||||
tx, _ = store.db.Begin()
|
tx, _ = store.db.Begin()
|
||||||
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
||||||
|
@ -120,7 +120,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The oldest cache entry should now become at ~8 hours old, because this entry is older
|
// The oldest cache entry should now become at ~8 hours old, because this entry is older
|
||||||
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})
|
store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})
|
||||||
|
|
||||||
tx, _ = store.db.Begin()
|
tx, _ = store.db.Begin()
|
||||||
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
||||||
|
@ -130,7 +130,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since this is one hour before reaching the clean up threshold, the oldest entry should now be this one
|
// Since this is one hour before reaching the clean up threshold, the oldest entry should now be this one
|
||||||
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold - time.Hour)), Success: true})
|
store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold - time.Hour)), Success: true})
|
||||||
|
|
||||||
tx, _ = store.db.Begin()
|
tx, _ = store.db.Begin()
|
||||||
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
||||||
|
@ -141,7 +141,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
|
||||||
|
|
||||||
// Since this entry is after the uptimeCleanUpThreshold, both this entry as well as the previous
|
// Since this entry is after the uptimeCleanUpThreshold, both this entry as well as the previous
|
||||||
// one should be deleted since they both surpass uptimeRetention
|
// one should be deleted since they both surpass uptimeRetention
|
||||||
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold + time.Hour)), Success: true})
|
store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold + time.Hour)), Success: true})
|
||||||
|
|
||||||
tx, _ = store.db.Begin()
|
tx, _ = store.db.Begin()
|
||||||
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
||||||
|
@ -313,7 +313,7 @@ func TestStore_InvalidTransaction(t *testing.T) {
|
||||||
if _, err := store.insertEndpoint(tx, &testEndpoint); err == nil {
|
if _, err := store.insertEndpoint(tx, &testEndpoint); err == nil {
|
||||||
t.Error("should've returned an error, because the transaction was already committed")
|
t.Error("should've returned an error, because the transaction was already committed")
|
||||||
}
|
}
|
||||||
if err := store.insertEndpointEvent(tx, 1, core.NewEventFromResult(&testSuccessfulResult)); err == nil {
|
if err := store.insertEndpointEvent(tx, 1, endpoint.NewEventFromResult(&testSuccessfulResult)); err == nil {
|
||||||
t.Error("should've returned an error, because the transaction was already committed")
|
t.Error("should've returned an error, because the transaction was already committed")
|
||||||
}
|
}
|
||||||
if err := store.insertEndpointResult(tx, 1, &testSuccessfulResult); err == nil {
|
if err := store.insertEndpointResult(tx, 1, &testSuccessfulResult); err == nil {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage"
|
"github.com/TwiN/gatus/v5/storage"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/memory"
|
"github.com/TwiN/gatus/v5/storage/store/memory"
|
||||||
|
@ -14,15 +14,15 @@ import (
|
||||||
|
|
||||||
// Store is the interface that each store should implement
|
// Store is the interface that each store should implement
|
||||||
type Store interface {
|
type Store interface {
|
||||||
// GetAllEndpointStatuses returns the JSON encoding of all monitored core.EndpointStatus
|
// GetAllEndpointStatuses returns the JSON encoding of all monitored endpoint.Status
|
||||||
// with a subset of core.Result defined by the page and pageSize parameters
|
// with a subset of endpoint.Result defined by the page and pageSize parameters
|
||||||
GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*core.EndpointStatus, error)
|
GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error)
|
||||||
|
|
||||||
// GetEndpointStatus returns the endpoint status for a given endpoint name in the given group
|
// GetEndpointStatus returns the endpoint status for a given endpoint name in the given group
|
||||||
GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error)
|
GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error)
|
||||||
|
|
||||||
// GetEndpointStatusByKey returns the endpoint status for a given key
|
// GetEndpointStatusByKey returns the endpoint status for a given key
|
||||||
GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error)
|
GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*endpoint.Status, error)
|
||||||
|
|
||||||
// GetUptimeByKey returns the uptime percentage during a time range
|
// GetUptimeByKey returns the uptime percentage during a time range
|
||||||
GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
||||||
|
@ -34,9 +34,9 @@ type Store interface {
|
||||||
GetHourlyAverageResponseTimeByKey(key string, from, to time.Time) (map[int64]int, error)
|
GetHourlyAverageResponseTimeByKey(key string, from, to time.Time) (map[int64]int, error)
|
||||||
|
|
||||||
// Insert adds the observed result for the specified endpoint into the store
|
// Insert adds the observed result for the specified endpoint into the store
|
||||||
Insert(endpoint *core.Endpoint, result *core.Result) error
|
Insert(ep *endpoint.Endpoint, result *endpoint.Result) error
|
||||||
|
|
||||||
// DeleteAllEndpointStatusesNotInKeys removes all EndpointStatus that are not within the keys provided
|
// DeleteAllEndpointStatusesNotInKeys removes all Status that are not within the keys provided
|
||||||
//
|
//
|
||||||
// Used to delete endpoints that have been persisted but are no longer part of the configured endpoints
|
// Used to delete endpoints that have been persisted but are no longer part of the configured endpoints
|
||||||
DeleteAllEndpointStatusesNotInKeys(keys []string) int
|
DeleteAllEndpointStatusesNotInKeys(keys []string) int
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/memory"
|
"github.com/TwiN/gatus/v5/storage/store/memory"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/sql"
|
"github.com/TwiN/gatus/v5/storage/store/sql"
|
||||||
|
@ -53,11 +53,11 @@ func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) {
|
||||||
for _, numberOfEndpointsToCreate := range numberOfEndpoints {
|
for _, numberOfEndpointsToCreate := range numberOfEndpoints {
|
||||||
// Create endpoints and insert results
|
// Create endpoints and insert results
|
||||||
for i := 0; i < numberOfEndpointsToCreate; i++ {
|
for i := 0; i < numberOfEndpointsToCreate; i++ {
|
||||||
endpoint := testEndpoint
|
ep := testEndpoint
|
||||||
endpoint.Name = "endpoint" + strconv.Itoa(i)
|
ep.Name = "endpoint" + strconv.Itoa(i)
|
||||||
// Insert 20 results for each endpoint
|
// Insert 20 results for each endpoint
|
||||||
for j := 0; j < 20; j++ {
|
for j := 0; j < 20; j++ {
|
||||||
scenario.Store.Insert(&endpoint, &testSuccessfulResult)
|
scenario.Store.Insert(&ep, &testSuccessfulResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Run the scenarios
|
// Run the scenarios
|
||||||
|
@ -123,7 +123,7 @@ func BenchmarkStore_Insert(b *testing.B) {
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
n := 0
|
n := 0
|
||||||
for pb.Next() {
|
for pb.Next() {
|
||||||
var result core.Result
|
var result endpoint.Result
|
||||||
if n%10 == 0 {
|
if n%10 == 0 {
|
||||||
result = testUnsuccessfulResult
|
result = testUnsuccessfulResult
|
||||||
} else {
|
} else {
|
||||||
|
@ -136,7 +136,7 @@ func BenchmarkStore_Insert(b *testing.B) {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
var result core.Result
|
var result endpoint.Result
|
||||||
if n%10 == 0 {
|
if n%10 == 0 {
|
||||||
result = testUnsuccessfulResult
|
result = testUnsuccessfulResult
|
||||||
} else {
|
} else {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue