mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-01 04:14:20 +01:00
Merge branch 'master' into RawUptimeData
This commit is contained in:
commit
5880be796d
@ -10,7 +10,7 @@ var (
|
||||
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 of alert (required)
|
||||
Type Type `yaml:"type"`
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
@ -57,14 +57,14 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// 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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
svc := ses.New(sess)
|
||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
||||
emails := strings.Split(provider.getToForGroup(endpoint.Group), ",")
|
||||
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
|
||||
emails := strings.Split(provider.getToForGroup(ep.Group), ",")
|
||||
|
||||
input := &ses.SendEmailInput{
|
||||
Destination: &ses.Destination{
|
||||
@ -110,14 +110,14 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
}
|
||||
|
||||
// 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
|
||||
if resolved {
|
||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||
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", ep.DisplayName(), alert.SuccessThreshold)
|
||||
} else {
|
||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.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)
|
||||
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", ep.DisplayName(), alert.FailureThreshold)
|
||||
}
|
||||
var formattedConditionResults string
|
||||
if len(result.ConditionResults) > 0 {
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
@ -95,10 +95,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", 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/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
|
||||
@ -50,16 +50,16 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri
|
||||
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 = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpoint.Name)
|
||||
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", endpoint.Name)
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", endpoint.Group)
|
||||
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", endpoint.Group)
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", endpoint.URL)
|
||||
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", endpoint.URL)
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", ep.Name)
|
||||
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", ep.Name)
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", ep.Group)
|
||||
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", ep.Group)
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", ep.URL)
|
||||
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", ep.URL)
|
||||
if resolved {
|
||||
body = strings.ReplaceAll(body, "[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
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
request := provider.buildHTTPRequest(endpoint, alert, resolved)
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
request := provider.buildHTTPRequest(ep, alert, resolved)
|
||||
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -90,10 +90,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -138,7 +138,7 @@ func TestAlertProvider_buildHTTPRequest(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||
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},
|
||||
scenario.Resolved,
|
||||
)
|
||||
@ -188,7 +188,7 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||
request := customAlertProvider.buildHTTPRequest(
|
||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&alert.Alert{Description: &alertDescription},
|
||||
scenario.Resolved,
|
||||
)
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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
|
||||
@ -47,9 +47,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -85,14 +85,14 @@ type Field struct {
|
||||
}
|
||||
|
||||
// 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 colorCode int
|
||||
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
|
||||
} 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
|
||||
}
|
||||
var formattedConditionResults string
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -127,10 +127,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -191,18 +191,18 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
var conditionResults []*core.ConditionResult
|
||||
var conditionResults []*endpoint.ConditionResult
|
||||
if !scenario.NoConditions {
|
||||
conditionResults = []*core.ConditionResult{
|
||||
conditionResults = []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
{Condition: "[BODY] != \"\"", Success: scenario.Resolved},
|
||||
}
|
||||
}
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
&endpoint.Result{
|
||||
ConditionResults: conditionResults,
|
||||
},
|
||||
scenario.Resolved,
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
gomail "gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
@ -53,17 +53,17 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// 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
|
||||
if len(provider.Username) > 0 {
|
||||
username = provider.Username
|
||||
} else {
|
||||
username = provider.From
|
||||
}
|
||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
||||
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
|
||||
m := gomail.NewMessage()
|
||||
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.SetBody("text/plain", body)
|
||||
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
|
||||
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
|
||||
if resolved {
|
||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||
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", ep.DisplayName(), alert.SuccessThreshold)
|
||||
} else {
|
||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.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)
|
||||
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", ep.DisplayName(), alert.FailureThreshold)
|
||||
}
|
||||
var formattedConditionResults string
|
||||
if len(result.ConditionResults) > 0 {
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
@ -97,10 +97,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"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,
|
||||
// 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 {
|
||||
title := "alert(gatus): " + endpoint.DisplayName()
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
title := "alert(gatus): " + ep.DisplayName()
|
||||
if !resolved {
|
||||
_, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
|
||||
Title: github.String(title),
|
||||
Body: github.String(provider.buildIssueBody(endpoint, alert, result)),
|
||||
Body: github.String(provider.buildIssueBody(ep, alert, result)),
|
||||
})
|
||||
if err != nil {
|
||||
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
|
||||
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
|
||||
if len(result.ConditionResults) > 0 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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/google/go-github/v48/github"
|
||||
)
|
||||
@ -85,10 +85,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
scenario.Provider.githubClient = github.NewClient(nil)
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -109,7 +109,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Endpoint core.Endpoint
|
||||
Endpoint endpoint.Endpoint
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
NoConditions bool
|
||||
@ -117,14 +117,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Provider: AlertProvider{},
|
||||
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`",
|
||||
},
|
||||
{
|
||||
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{},
|
||||
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`",
|
||||
@ -132,7 +132,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
{
|
||||
Name: "triggered-with-no-conditions",
|
||||
NoConditions: true,
|
||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Provider: AlertProvider{},
|
||||
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",
|
||||
@ -140,9 +140,9 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
var conditionResults []*core.ConditionResult
|
||||
var conditionResults []*endpoint.ConditionResult
|
||||
if !scenario.NoConditions {
|
||||
conditionResults = []*core.ConditionResult{
|
||||
conditionResults = []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: true},
|
||||
{Condition: "[STATUS] == 200", Success: false},
|
||||
}
|
||||
@ -150,7 +150,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
body := scenario.Provider.buildIssueBody(
|
||||
&scenario.Endpoint,
|
||||
&scenario.Alert,
|
||||
&core.Result{ConditionResults: conditionResults},
|
||||
&endpoint.Result{ConditionResults: conditionResults},
|
||||
)
|
||||
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
|
||||
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/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
"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,
|
||||
// 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 {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -94,21 +94,21 @@ func (provider *AlertProvider) monitoringTool() string {
|
||||
return "gatus"
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) service(endpoint *core.Endpoint) string {
|
||||
func (provider *AlertProvider) service(ep *endpoint.Endpoint) string {
|
||||
if len(provider.Service) > 0 {
|
||||
return provider.Service
|
||||
}
|
||||
return endpoint.DisplayName()
|
||||
return ep.DisplayName()
|
||||
}
|
||||
|
||||
// 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{
|
||||
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),
|
||||
Service: provider.service(endpoint),
|
||||
Service: provider.service(ep),
|
||||
MonitoringTool: provider.monitoringTool(),
|
||||
Hosts: endpoint.URL,
|
||||
Hosts: ep.URL,
|
||||
GitlabEnvironmentName: provider.EnvironmentName,
|
||||
Severity: provider.Severity,
|
||||
Fingerprint: alert.ResolveKey,
|
||||
@ -135,9 +135,9 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al
|
||||
}
|
||||
var message string
|
||||
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 {
|
||||
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
|
||||
bodyAsJSON, _ := json.Marshal(body)
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -84,10 +84,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -108,21 +108,21 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Endpoint core.Endpoint
|
||||
Endpoint endpoint.Endpoint
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Provider: AlertProvider{},
|
||||
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\"}",
|
||||
},
|
||||
{
|
||||
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{},
|
||||
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\"}",
|
||||
@ -133,8 +133,8 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
|
||||
body := scenario.Provider.buildAlertBody(
|
||||
&scenario.Endpoint,
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: true},
|
||||
{Condition: "[STATUS] == 200", Success: false},
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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
|
||||
@ -50,9 +50,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -112,7 +112,7 @@ type OpenLink struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
if resolved {
|
||||
color = "#36A64F"
|
||||
@ -143,7 +143,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
Widgets: []Widgets{
|
||||
{
|
||||
KeyValue: &KeyValue{
|
||||
TopLabel: endpoint.DisplayName(),
|
||||
TopLabel: ep.DisplayName(),
|
||||
Content: message,
|
||||
ContentMultiline: "true",
|
||||
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
|
||||
// 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
|
||||
@ -175,7 +175,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
{
|
||||
TextButton: TextButton{
|
||||
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/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
"github.com/TwiN/gatus/v5/test"
|
||||
)
|
||||
|
||||
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -141,7 +141,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Endpoint core.Endpoint
|
||||
Endpoint endpoint.Endpoint
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
@ -149,7 +149,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
@ -157,7 +157,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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
|
||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"},
|
||||
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"},
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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
|
||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"},
|
||||
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"},
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
@ -185,8 +185,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&scenario.Endpoint,
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", 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/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
)
|
||||
|
||||
const DefaultPriority = 5
|
||||
@ -41,8 +41,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -67,12 +67,12 @@ type Body struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
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 {
|
||||
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
|
||||
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 += formattedConditionResults
|
||||
title := "Gatus: " + endpoint.DisplayName()
|
||||
title := "Gatus: " + ep.DisplayName()
|
||||
if provider.Title != "" {
|
||||
title = provider.Title
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
@ -49,7 +49,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
var (
|
||||
description = "custom-description"
|
||||
//title = "custom-title"
|
||||
endpoint = "custom-endpoint"
|
||||
endpointName = "custom-endpoint"
|
||||
)
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
@ -63,30 +63,30 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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",
|
||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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",
|
||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
|
||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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 {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: endpoint},
|
||||
&endpoint.Endpoint{Name: endpointName},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", 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/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
|
||||
@ -46,8 +46,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project)
|
||||
request, err := http.NewRequest(http.MethodPost, url, buffer)
|
||||
if err != nil {
|
||||
@ -103,9 +103,9 @@ type Icon struct {
|
||||
}
|
||||
|
||||
// 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{
|
||||
Channel: "id:" + provider.getChannelIDForGroup(endpoint.Group),
|
||||
Channel: "id:" + provider.getChannelIDForGroup(ep.Group),
|
||||
Content: Content{
|
||||
ClassName: "ChatMessage.Block",
|
||||
Sections: []Section{{
|
||||
@ -116,10 +116,10 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}
|
||||
if resolved {
|
||||
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 {
|
||||
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 {
|
||||
icon := "warning"
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -120,10 +120,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -146,7 +146,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Endpoint core.Endpoint
|
||||
Endpoint endpoint.Endpoint
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
@ -154,7 +154,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{},
|
||||
Endpoint: core.Endpoint{Name: "name"},
|
||||
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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"}]}}`,
|
||||
@ -162,7 +162,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
{
|
||||
Name: "triggered-with-group",
|
||||
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},
|
||||
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"}]}}`,
|
||||
@ -170,7 +170,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Endpoint: core.Endpoint{Name: "name"},
|
||||
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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"}]}}`,
|
||||
@ -178,7 +178,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
{
|
||||
Name: "resolved-with-group",
|
||||
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},
|
||||
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"}]}}`,
|
||||
@ -189,8 +189,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&scenario.Endpoint,
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", 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/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
|
||||
@ -61,9 +61,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
config := provider.getConfigForGroup(endpoint.Group)
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
config := provider.getConfigForGroup(ep.Group)
|
||||
if config.ServerURL == "" {
|
||||
config.ServerURL = defaultServerURL
|
||||
}
|
||||
@ -103,23 +103,23 @@ type Body struct {
|
||||
}
|
||||
|
||||
// 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{
|
||||
MsgType: "m.text",
|
||||
Format: "org.matrix.custom.html",
|
||||
Body: buildPlaintextMessageBody(endpoint, alert, result, resolved),
|
||||
FormattedBody: buildHTMLMessageBody(endpoint, alert, result, resolved),
|
||||
Body: buildPlaintextMessageBody(ep, alert, result, resolved),
|
||||
FormattedBody: buildHTMLMessageBody(ep, alert, result, resolved),
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// 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
|
||||
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 {
|
||||
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
|
||||
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
|
||||
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
|
||||
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 {
|
||||
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
|
||||
if len(result.ConditionResults) > 0 {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -149,10 +149,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -197,10 +197,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", 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/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
|
||||
@ -50,9 +50,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -92,13 +92,13 @@ type Field struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
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"
|
||||
} 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"
|
||||
}
|
||||
var formattedConditionResults string
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -120,10 +120,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -168,10 +168,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", 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/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -33,8 +33,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
|
||||
// Send an alert using the provider
|
||||
// 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 {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -60,12 +60,12 @@ type Body struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||
} 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{
|
||||
Originator: provider.Originator,
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -83,10 +83,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -131,10 +131,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", 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/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -46,8 +46,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -77,7 +77,7 @@ type Body struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
if resolved {
|
||||
tag = "white_check_mark"
|
||||
@ -101,7 +101,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
message += formattedConditionResults
|
||||
body, _ := json.Marshal(Body{
|
||||
Topic: provider.Topic,
|
||||
Title: "Gatus: " + endpoint.DisplayName(),
|
||||
Title: "Gatus: " + ep.DisplayName(),
|
||||
Message: message,
|
||||
Tags: []string{tag},
|
||||
Priority: provider.Priority,
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
@ -92,10 +92,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", 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/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -59,13 +59,13 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
// Send an alert using the provider
|
||||
//
|
||||
// Relevant: https://docs.opsgenie.com/docs/alert-api
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
err := provider.createAlert(endpoint, alert, result, resolved)
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
err := provider.createAlert(ep, alert, result, resolved)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resolved {
|
||||
err = provider.closeAlert(endpoint, alert)
|
||||
err = provider.closeAlert(ep, alert)
|
||||
if err != nil {
|
||||
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
|
||||
alert.ResolveKey = ""
|
||||
} else {
|
||||
alert.ResolveKey = provider.alias(buildKey(endpoint))
|
||||
alert.ResolveKey = provider.alias(buildKey(ep))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) createAlert(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
payload := provider.buildCreateRequestBody(endpoint, alert, result, resolved)
|
||||
func (provider *AlertProvider) createAlert(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
payload := provider.buildCreateRequestBody(ep, alert, result, resolved)
|
||||
return provider.sendRequest(restAPI, http.MethodPost, payload)
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) closeAlert(endpoint *core.Endpoint, alert *alert.Alert) error {
|
||||
payload := provider.buildCloseRequestBody(endpoint, alert)
|
||||
url := restAPI + "/" + provider.alias(buildKey(endpoint)) + "/close?identifierType=alias"
|
||||
func (provider *AlertProvider) closeAlert(ep *endpoint.Endpoint, alert *alert.Alert) error {
|
||||
payload := provider.buildCloseRequestBody(ep, alert)
|
||||
url := restAPI + "/" + provider.alias(buildKey(ep)) + "/close?identifierType=alias"
|
||||
return provider.sendRequest(url, http.MethodPost, payload)
|
||||
}
|
||||
|
||||
@ -115,17 +115,17 @@ func (provider *AlertProvider) sendRequest(url, method string, payload interface
|
||||
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
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.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)
|
||||
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", ep.DisplayName(), alert.SuccessThreshold)
|
||||
} else {
|
||||
message = fmt.Sprintf("%s - %s", endpoint.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)
|
||||
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", ep.DisplayName(), alert.FailureThreshold)
|
||||
}
|
||||
if endpoint.Group != "" {
|
||||
message = fmt.Sprintf("[%s] %s", endpoint.Group, message)
|
||||
if ep.Group != "" {
|
||||
message = fmt.Sprintf("[%s] %s", ep.Group, message)
|
||||
}
|
||||
var formattedConditionResults string
|
||||
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)
|
||||
}
|
||||
description = description + "\n" + formattedConditionResults
|
||||
key := buildKey(endpoint)
|
||||
key := buildKey(ep)
|
||||
details := map[string]string{
|
||||
"endpoint:url": endpoint.URL,
|
||||
"endpoint:group": endpoint.Group,
|
||||
"endpoint:url": ep.URL,
|
||||
"endpoint:group": ep.Group,
|
||||
"result:hostname": result.Hostname,
|
||||
"result:ip": result.IP,
|
||||
"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{
|
||||
Source: buildKey(endpoint),
|
||||
Note: fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()),
|
||||
Source: buildKey(ep),
|
||||
Note: fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,12 +211,12 @@ func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
func buildKey(endpoint *core.Endpoint) string {
|
||||
name := toKebabCase(endpoint.Name)
|
||||
if endpoint.Group == "" {
|
||||
func buildKey(ep *endpoint.Endpoint) string {
|
||||
name := toKebabCase(ep.Name)
|
||||
if ep.Group == "" {
|
||||
return name
|
||||
}
|
||||
return toKebabCase(endpoint.Group) + "-" + name
|
||||
return toKebabCase(ep.Group) + "-" + name
|
||||
}
|
||||
|
||||
func toKebabCase(val string) string {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -79,10 +79,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -106,8 +106,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||
Name string
|
||||
Provider *AlertProvider
|
||||
Alert *alert.Alert
|
||||
Endpoint *core.Endpoint
|
||||
Result *core.Result
|
||||
Endpoint *endpoint.Endpoint
|
||||
Result *endpoint.Result
|
||||
Resolved bool
|
||||
want alertCreateRequest
|
||||
}{
|
||||
@ -115,8 +115,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||
Name: "missing all params (unresolved)",
|
||||
Provider: &AlertProvider{},
|
||||
Alert: &alert.Alert{},
|
||||
Endpoint: &core.Endpoint{},
|
||||
Result: &core.Result{},
|
||||
Endpoint: &endpoint.Endpoint{},
|
||||
Result: &endpoint.Result{},
|
||||
Resolved: false,
|
||||
want: alertCreateRequest{
|
||||
Message: " - ",
|
||||
@ -133,8 +133,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||
Name: "missing all params (resolved)",
|
||||
Provider: &AlertProvider{},
|
||||
Alert: &alert.Alert{},
|
||||
Endpoint: &core.Endpoint{},
|
||||
Result: &core.Result{},
|
||||
Endpoint: &endpoint.Endpoint{},
|
||||
Result: &endpoint.Result{},
|
||||
Resolved: true,
|
||||
want: alertCreateRequest{
|
||||
Message: "RESOLVED: - ",
|
||||
@ -154,11 +154,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||
Description: &description,
|
||||
FailureThreshold: 3,
|
||||
},
|
||||
Endpoint: &core.Endpoint{
|
||||
Endpoint: &endpoint.Endpoint{
|
||||
Name: "my super app",
|
||||
},
|
||||
Result: &core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
Result: &endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
@ -194,11 +194,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||
Description: &description,
|
||||
SuccessThreshold: 4,
|
||||
},
|
||||
Endpoint: &core.Endpoint{
|
||||
Endpoint: &endpoint.Endpoint{
|
||||
Name: "my mega app",
|
||||
},
|
||||
Result: &core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
Result: &endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
@ -226,17 +226,17 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
||||
Description: &description,
|
||||
FailureThreshold: 6,
|
||||
},
|
||||
Endpoint: &core.Endpoint{
|
||||
Endpoint: &endpoint.Endpoint{
|
||||
Name: "my app",
|
||||
Group: "end game",
|
||||
URL: "https://my.go/app",
|
||||
},
|
||||
Result: &core.Result{
|
||||
Result: &endpoint.Result{
|
||||
HTTPStatus: 400,
|
||||
Hostname: "my.go",
|
||||
Errors: []string{"error 01", "error 02"},
|
||||
Success: false,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: false,
|
||||
@ -279,14 +279,14 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
|
||||
Name string
|
||||
Provider *AlertProvider
|
||||
Alert *alert.Alert
|
||||
Endpoint *core.Endpoint
|
||||
Endpoint *endpoint.Endpoint
|
||||
want alertCloseRequest
|
||||
}{
|
||||
{
|
||||
Name: "Missing all values",
|
||||
Provider: &AlertProvider{},
|
||||
Alert: &alert.Alert{},
|
||||
Endpoint: &core.Endpoint{},
|
||||
Endpoint: &endpoint.Endpoint{},
|
||||
want: alertCloseRequest{
|
||||
Source: "",
|
||||
Note: "RESOLVED: - ",
|
||||
@ -298,7 +298,7 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
|
||||
Alert: &alert.Alert{
|
||||
Description: &description,
|
||||
},
|
||||
Endpoint: &core.Endpoint{
|
||||
Endpoint: &endpoint.Endpoint{
|
||||
Name: "endpoint name",
|
||||
},
|
||||
want: alertCloseRequest{
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
// Send an alert using the provider
|
||||
//
|
||||
// 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 {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -101,19 +101,19 @@ type Payload struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||
eventAction = "resolve"
|
||||
resolveKey = alert.ResolveKey
|
||||
} else {
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||
eventAction = "trigger"
|
||||
resolveKey = ""
|
||||
}
|
||||
body, _ := json.Marshal(Body{
|
||||
RoutingKey: provider.getIntegrationKeyForGroup(endpoint.Group),
|
||||
RoutingKey: provider.getIntegrationKeyForGroup(ep.Group),
|
||||
DedupKey: resolveKey,
|
||||
EventAction: eventAction,
|
||||
Payload: Payload{
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -115,10 +115,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -161,7 +161,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
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 {
|
||||
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/telegram"
|
||||
"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
|
||||
@ -33,7 +33,7 @@ type AlertProvider interface {
|
||||
GetDefaultAlert() *alert.Alert
|
||||
|
||||
// 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
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
|
||||
// Send an alert using the provider
|
||||
// Reference doc for pushover: https://pushover.net/api
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -81,12 +81,12 @@ type Body struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||
} 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{
|
||||
Token: provider.ApplicationToken,
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -95,10 +95,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -150,10 +150,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", 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/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
|
||||
@ -42,9 +42,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -81,13 +81,13 @@ type Field struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
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"
|
||||
} 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"
|
||||
}
|
||||
var formattedConditionResults string
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -142,7 +142,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Endpoint core.Endpoint
|
||||
Endpoint endpoint.Endpoint
|
||||
Alert alert.Alert
|
||||
NoConditions bool
|
||||
Resolved bool
|
||||
@ -151,7 +151,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{},
|
||||
Endpoint: core.Endpoint{Name: "name"},
|
||||
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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}]}]}",
|
||||
@ -159,7 +159,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
{
|
||||
Name: "triggered-with-group",
|
||||
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},
|
||||
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}]}]}",
|
||||
@ -168,7 +168,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Name: "triggered-with-no-conditions",
|
||||
NoConditions: true,
|
||||
Provider: AlertProvider{},
|
||||
Endpoint: core.Endpoint{Name: "name"},
|
||||
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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\"}]}",
|
||||
@ -176,7 +176,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Endpoint: core.Endpoint{Name: "name"},
|
||||
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
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}]}]}",
|
||||
@ -184,7 +184,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
{
|
||||
Name: "resolved-with-group",
|
||||
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},
|
||||
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}]}]}",
|
||||
@ -192,9 +192,9 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
var conditionResults []*core.ConditionResult
|
||||
var conditionResults []*endpoint.ConditionResult
|
||||
if !scenario.NoConditions {
|
||||
conditionResults = []*core.ConditionResult{
|
||||
conditionResults = []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
}
|
||||
@ -202,7 +202,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&scenario.Endpoint,
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
&endpoint.Result{
|
||||
ConditionResults: conditionResults,
|
||||
},
|
||||
scenario.Resolved,
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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
|
||||
@ -47,9 +47,9 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -81,13 +81,13 @@ type Section struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
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"
|
||||
} 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"
|
||||
}
|
||||
var formattedConditionResults string
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -172,17 +172,17 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
var conditionResults []*core.ConditionResult
|
||||
var conditionResults []*endpoint.ConditionResult
|
||||
if !scenario.NoConditions {
|
||||
conditionResults = []*core.ConditionResult{
|
||||
conditionResults = []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
}
|
||||
}
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{ConditionResults: conditionResults},
|
||||
&endpoint.Result{ConditionResults: conditionResults},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if string(body) != scenario.ExpectedBody {
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
@ -36,8 +36,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||
apiURL := provider.APIURL
|
||||
if apiURL == "" {
|
||||
apiURL = defaultAPIURL
|
||||
@ -66,12 +66,12 @@ type Body struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
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 {
|
||||
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
|
||||
if len(result.ConditionResults) > 0 {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -89,10 +89,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
@ -145,17 +145,17 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
var conditionResults []*core.ConditionResult
|
||||
var conditionResults []*endpoint.ConditionResult
|
||||
if !scenario.NoConditions {
|
||||
conditionResults = []*core.ConditionResult{
|
||||
conditionResults = []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
}
|
||||
}
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{ConditionResults: conditionResults},
|
||||
&endpoint.Result{ConditionResults: conditionResults},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if string(body) != scenario.ExpectedBody {
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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
|
||||
@ -30,8 +30,8 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -51,12 +51,12 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
}
|
||||
|
||||
// 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
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||
} else {
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||
}
|
||||
return url.Values{
|
||||
"To": {provider.To},
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
@ -51,10 +51,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
&endpoint.Result{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/common"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/core/ui"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||
"github.com/TwiN/gatus/v5/storage/store"
|
||||
"github.com/TwiN/gatus/v5/watchdog"
|
||||
)
|
||||
@ -19,7 +19,7 @@ func TestBadge(t *testing.T) {
|
||||
defer cache.Clear()
|
||||
cfg := &config.Config{
|
||||
Metrics: true,
|
||||
Endpoints: []*core.Endpoint{
|
||||
Endpoints: []*endpoint.Endpoint{
|
||||
{
|
||||
Name: "frontend",
|
||||
Group: "core",
|
||||
@ -34,8 +34,8 @@ func TestBadge(t *testing.T) {
|
||||
cfg.Endpoints[0].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[1], &core.Result{Success: false, Connected: false, Duration: time.Second, 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], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
api := New(cfg)
|
||||
router := api.Router()
|
||||
type Scenario struct {
|
||||
@ -218,30 +218,30 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||
defer cache.Clear()
|
||||
|
||||
var (
|
||||
firstCondition = core.Condition("[STATUS] == 200")
|
||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||
firstCondition = endpoint.Condition("[STATUS] == 200")
|
||||
secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
|
||||
thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||
)
|
||||
|
||||
firstTestEndpoint := core.Endpoint{
|
||||
firstTestEndpoint := endpoint.Endpoint{
|
||||
Name: "a",
|
||||
URL: "https://example.org/what/ever",
|
||||
Method: "GET",
|
||||
Body: "body",
|
||||
Interval: 30 * time.Second,
|
||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Alerts: nil,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
UIConfig: ui.GetDefaultConfig(),
|
||||
}
|
||||
secondTestEndpoint := core.Endpoint{
|
||||
secondTestEndpoint := endpoint.Endpoint{
|
||||
Name: "b",
|
||||
URL: "https://example.org/what/ever",
|
||||
Method: "GET",
|
||||
Body: "body",
|
||||
Interval: 30 * time.Second,
|
||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Alerts: nil,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
@ -255,10 +255,10 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||
}
|
||||
cfg := &config.Config{
|
||||
Metrics: true,
|
||||
Endpoints: []*core.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
||||
Endpoints: []*endpoint.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
||||
}
|
||||
|
||||
testSuccessfulResult := core.Result{
|
||||
testSuccessfulResult := endpoint.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
@ -268,7 +268,7 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||
Timestamp: time.Now(),
|
||||
Duration: 150 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/watchdog"
|
||||
)
|
||||
@ -17,7 +17,7 @@ func TestResponseTimeChart(t *testing.T) {
|
||||
defer cache.Clear()
|
||||
cfg := &config.Config{
|
||||
Metrics: true,
|
||||
Endpoints: []*core.Endpoint{
|
||||
Endpoints: []*endpoint.Endpoint{
|
||||
{
|
||||
Name: "frontend",
|
||||
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[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
api := New(cfg)
|
||||
router := api.Router()
|
||||
type Scenario struct {
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"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/core"
|
||||
"github.com/TwiN/gatus/v5/storage/store"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||
"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 {
|
||||
return nil, nil
|
||||
}
|
||||
var endpointStatusesFromAllRemotes []*core.EndpointStatus
|
||||
var endpointStatusesFromAllRemotes []*endpoint.Status
|
||||
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
|
||||
for _, instance := range remoteConfig.Instances {
|
||||
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())
|
||||
continue
|
||||
}
|
||||
var endpointStatuses []*core.EndpointStatus
|
||||
var endpointStatuses []*endpoint.Status
|
||||
if err = json.Unmarshal(body, &endpointStatuses); err != nil {
|
||||
_ = response.Body.Close()
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
page, pageSize := extractPageAndPageSizeFromRequest(c)
|
||||
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/watchdog"
|
||||
)
|
||||
@ -16,19 +16,19 @@ import (
|
||||
var (
|
||||
timestamp = time.Now()
|
||||
|
||||
testEndpoint = core.Endpoint{
|
||||
testEndpoint = endpoint.Endpoint{
|
||||
Name: "name",
|
||||
Group: "group",
|
||||
URL: "https://example.org/what/ever",
|
||||
Method: "GET",
|
||||
Body: "body",
|
||||
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,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
}
|
||||
testSuccessfulResult = core.Result{
|
||||
testSuccessfulResult = endpoint.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
@ -38,7 +38,7 @@ var (
|
||||
Timestamp: timestamp,
|
||||
Duration: 150 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
@ -53,7 +53,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
testUnsuccessfulResult = core.Result{
|
||||
testUnsuccessfulResult = endpoint.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
@ -63,7 +63,7 @@ var (
|
||||
Timestamp: timestamp,
|
||||
Duration: 750 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
@ -85,7 +85,7 @@ func TestEndpointStatus(t *testing.T) {
|
||||
defer cache.Clear()
|
||||
cfg := &config.Config{
|
||||
Metrics: true,
|
||||
Endpoints: []*core.Endpoint{
|
||||
Endpoints: []*endpoint.Endpoint{
|
||||
{
|
||||
Name: "frontend",
|
||||
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[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
api := New(cfg)
|
||||
router := api.Router()
|
||||
type Scenario struct {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/common"
|
||||
"github.com/TwiN/gatus/v5/watchdog"
|
||||
@ -41,7 +41,7 @@ func CreateExternalEndpointResult(cfg *config.Config) fiber.Handler {
|
||||
return c.Status(401).SendString("invalid token")
|
||||
}
|
||||
// Persist the result in the storage
|
||||
result := &core.Result{
|
||||
result := &endpoint.Result{
|
||||
Timestamp: time.Now(),
|
||||
Success: c.QueryBool("success"),
|
||||
Errors: []string{},
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||
"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/core"
|
||||
"github.com/TwiN/gatus/v5/storage/store"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||
)
|
||||
@ -22,7 +22,7 @@ func TestCreateExternalEndpointResult(t *testing.T) {
|
||||
Alerting: &alerting.Config{
|
||||
Discord: &discord.AlertProvider{},
|
||||
},
|
||||
ExternalEndpoints: []*core.ExternalEndpoint{
|
||||
ExternalEndpoints: []*endpoint.ExternalEndpoint{
|
||||
{
|
||||
Name: "n",
|
||||
Group: "g",
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
"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/core"
|
||||
"github.com/TwiN/gatus/v5/storage/store"
|
||||
"github.com/TwiN/gatus/v5/watchdog"
|
||||
)
|
||||
@ -20,7 +20,7 @@ func TestSinglePageApplication(t *testing.T) {
|
||||
defer cache.Clear()
|
||||
cfg := &config.Config{
|
||||
Metrics: true,
|
||||
Endpoints: []*core.Endpoint{
|
||||
Endpoints: []*endpoint.Endpoint{
|
||||
{
|
||||
Name: "frontend",
|
||||
Group: "core",
|
||||
@ -34,8 +34,8 @@ func TestSinglePageApplication(t *testing.T) {
|
||||
Title: "example-title",
|
||||
},
|
||||
}
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.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[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
api := New(cfg)
|
||||
router := api.Router()
|
||||
type Scenario struct {
|
||||
|
@ -16,11 +16,16 @@ import (
|
||||
"github.com/TwiN/gocache/v2"
|
||||
"github.com/TwiN/whois"
|
||||
"github.com/ishidawataru/sctp"
|
||||
"github.com/miekg/dns"
|
||||
ping "github.com/prometheus-community/pro-bing"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsPort = 53
|
||||
)
|
||||
|
||||
var (
|
||||
// injectedHTTPClient is used for testing purposes
|
||||
injectedHTTPClient *http.Client
|
||||
@ -291,6 +296,49 @@ func QueryWebSocket(address, body string, config *Config) (bool, []byte, error)
|
||||
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
|
||||
func InjectHTTPClient(httpClient *http.Client) {
|
||||
injectedHTTPClient = httpClient
|
||||
|
@ -8,6 +8,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||
"github.com/TwiN/gatus/v5/pattern"
|
||||
"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/provider"
|
||||
"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/remote"
|
||||
"github.com/TwiN/gatus/v5/config/ui"
|
||||
"github.com/TwiN/gatus/v5/config/web"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/security"
|
||||
"github.com/TwiN/gatus/v5/storage"
|
||||
"github.com/TwiN/gatus/v5/util"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@ -74,10 +73,10 @@ type Config struct {
|
||||
Alerting *alerting.Config `yaml:"alerting,omitempty"`
|
||||
|
||||
// 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 []*core.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
|
||||
ExternalEndpoints []*endpoint.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
|
||||
|
||||
// Storage is the configuration for how the data is stored
|
||||
Storage *storage.Config `yaml:"storage,omitempty"`
|
||||
@ -102,20 +101,20 @@ type Config struct {
|
||||
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++ {
|
||||
ep := config.Endpoints[i]
|
||||
if util.ConvertGroupAndEndpointNameToKey(ep.Group, ep.Name) == key {
|
||||
if ep.Key() == key {
|
||||
return ep
|
||||
}
|
||||
}
|
||||
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++ {
|
||||
ee := config.ExternalEndpoints[i]
|
||||
if util.ConvertGroupAndEndpointNameToKey(ee.Group, ee.Name) == key {
|
||||
if ee.Key() == key {
|
||||
return ee
|
||||
}
|
||||
}
|
||||
@ -339,12 +338,12 @@ func validateWebConfig(config *Config) error {
|
||||
}
|
||||
|
||||
func validateEndpointsConfig(config *Config) error {
|
||||
for _, endpoint := range config.Endpoints {
|
||||
for _, ep := range config.Endpoints {
|
||||
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 {
|
||||
return fmt.Errorf("invalid endpoint %s: %w", endpoint.DisplayName(), err)
|
||||
if err := ep.ValidateAndSetDefaults(); err != nil {
|
||||
return fmt.Errorf("invalid endpoint %s: %w", ep.DisplayName(), err)
|
||||
}
|
||||
}
|
||||
log.Printf("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
|
||||
@ -352,12 +351,12 @@ func validateEndpointsConfig(config *Config) error {
|
||||
}
|
||||
|
||||
func validateExternalEndpointsConfig(config *Config) error {
|
||||
for _, externalEndpoint := range config.ExternalEndpoints {
|
||||
for _, ee := range config.ExternalEndpoints {
|
||||
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 {
|
||||
return fmt.Errorf("invalid external endpoint %s: %w", externalEndpoint.DisplayName(), err)
|
||||
if err := ee.ValidateAndSetDefaults(); err != nil {
|
||||
return fmt.Errorf("invalid external endpoint %s: %w", ee.DisplayName(), err)
|
||||
}
|
||||
}
|
||||
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
|
||||
// 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.
|
||||
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.Endpoint, debug bool) {
|
||||
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoint.Endpoint, debug bool) {
|
||||
if alertingConfig == nil {
|
||||
log.Printf("[config.validateAlertingConfig] Alerting is not configured")
|
||||
return
|
||||
@ -417,11 +416,11 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
||||
if alertProvider.IsValid() {
|
||||
// Parse alerts with the provider's default alert
|
||||
if alertProvider.GetDefaultAlert() != nil {
|
||||
for _, endpoint := range endpoints {
|
||||
for alertIndex, endpointAlert := range endpoint.Alerts {
|
||||
for _, ep := range endpoints {
|
||||
for alertIndex, endpointAlert := range ep.Alerts {
|
||||
if alertType == endpointAlert.Type {
|
||||
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)
|
||||
}
|
||||
|
@ -29,8 +29,8 @@ import (
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
||||
"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/core"
|
||||
"github.com/TwiN/gatus/v5/storage"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@ -65,7 +65,7 @@ func TestLoadConfiguration(t *testing.T) {
|
||||
endpoints:
|
||||
- name: website`,
|
||||
},
|
||||
expectedError: core.ErrEndpointWithNoURL,
|
||||
expectedError: endpoint.ErrEndpointWithNoURL,
|
||||
},
|
||||
{
|
||||
name: "config-file-with-endpoint-that-has-no-conditions",
|
||||
@ -76,7 +76,7 @@ endpoints:
|
||||
- name: website
|
||||
url: https://twin.sh/health`,
|
||||
},
|
||||
expectedError: core.ErrEndpointWithNoCondition,
|
||||
expectedError: endpoint.ErrEndpointWithNoCondition,
|
||||
},
|
||||
{
|
||||
name: "config-file",
|
||||
@ -90,11 +90,11 @@ endpoints:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
expectedConfig: &Config{
|
||||
Endpoints: []*core.Endpoint{
|
||||
Endpoints: []*endpoint.Endpoint{
|
||||
{
|
||||
Name: "website",
|
||||
URL: "https://twin.sh/health",
|
||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
||||
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -136,21 +136,21 @@ endpoints:
|
||||
- "[BODY].status == UP"`,
|
||||
},
|
||||
expectedConfig: &Config{
|
||||
Endpoints: []*core.Endpoint{
|
||||
Endpoints: []*endpoint.Endpoint{
|
||||
{
|
||||
Name: "one",
|
||||
URL: "https://example.com",
|
||||
Conditions: []core.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
|
||||
Conditions: []endpoint.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
|
||||
},
|
||||
{
|
||||
Name: "two",
|
||||
URL: "https://example.org",
|
||||
Conditions: []core.Condition{"len([BODY]) > 0"},
|
||||
Conditions: []endpoint.Condition{"len([BODY]) > 0"},
|
||||
},
|
||||
{
|
||||
Name: "three",
|
||||
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"},
|
||||
Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz"},
|
||||
},
|
||||
Endpoints: []*core.Endpoint{
|
||||
Endpoints: []*endpoint.Endpoint{
|
||||
{
|
||||
Name: "example",
|
||||
URL: "https://example.org",
|
||||
Interval: 5 * time.Second,
|
||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
||||
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||
},
|
||||
{
|
||||
Name: "frontend",
|
||||
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 {
|
||||
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 {
|
||||
t.Errorf("User-Agent should've been %s because it's the default value, got %s", core.GatusUserAgent, userAgent)
|
||||
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", endpoint.GatusUserAgent, userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"errors"
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"errors"
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"errors"
|
@ -1,6 +1,8 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
// ConditionResult result of a Condition
|
||||
type ConditionResult struct {
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"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 (
|
||||
"bytes"
|
||||
@ -15,12 +15,13 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core/ui"
|
||||
"github.com/TwiN/gatus/v5/util"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||
sshconfig "github.com/TwiN/gatus/v5/config/endpoint/ssh"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type EndpointType string
|
||||
type Type string
|
||||
|
||||
const (
|
||||
// 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 = "Gatus/1.0"
|
||||
|
||||
EndpointTypeDNS EndpointType = "DNS"
|
||||
EndpointTypeTCP EndpointType = "TCP"
|
||||
EndpointTypeSCTP EndpointType = "SCTP"
|
||||
EndpointTypeUDP EndpointType = "UDP"
|
||||
EndpointTypeICMP EndpointType = "ICMP"
|
||||
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
|
||||
EndpointTypeTLS EndpointType = "TLS"
|
||||
EndpointTypeHTTP EndpointType = "HTTP"
|
||||
EndpointTypeWS EndpointType = "WEBSOCKET"
|
||||
EndpointTypeSSH EndpointType = "SSH"
|
||||
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
|
||||
TypeDNS Type = "DNS"
|
||||
TypeTCP Type = "TCP"
|
||||
TypeSCTP Type = "SCTP"
|
||||
TypeUDP Type = "UDP"
|
||||
TypeICMP Type = "ICMP"
|
||||
TypeSTARTTLS Type = "STARTTLS"
|
||||
TypeTLS Type = "TLS"
|
||||
TypeHTTP Type = "HTTP"
|
||||
TypeWS Type = "WEBSOCKET"
|
||||
TypeSSH Type = "SSH"
|
||||
TypeUNKNOWN Type = "UNKNOWN"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -82,12 +83,6 @@ type Endpoint struct {
|
||||
// URL to send the request to
|
||||
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 string `yaml:"method,omitempty"`
|
||||
|
||||
@ -109,6 +104,12 @@ type Endpoint struct {
|
||||
// Alerts is the alerting configuration for the endpoint in case of failure
|
||||
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 *client.Config `yaml:"client,omitempty"`
|
||||
|
||||
@ -123,103 +124,103 @@ type Endpoint struct {
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the endpoint is enabled or not
|
||||
func (endpoint *Endpoint) IsEnabled() bool {
|
||||
if endpoint.Enabled == nil {
|
||||
func (e *Endpoint) IsEnabled() bool {
|
||||
if e.Enabled == nil {
|
||||
return true
|
||||
}
|
||||
return *endpoint.Enabled
|
||||
return *e.Enabled
|
||||
}
|
||||
|
||||
// Type returns the endpoint type
|
||||
func (endpoint *Endpoint) Type() EndpointType {
|
||||
func (e *Endpoint) Type() Type {
|
||||
switch {
|
||||
case endpoint.DNS != nil:
|
||||
return EndpointTypeDNS
|
||||
case strings.HasPrefix(endpoint.URL, "tcp://"):
|
||||
return EndpointTypeTCP
|
||||
case strings.HasPrefix(endpoint.URL, "sctp://"):
|
||||
return EndpointTypeSCTP
|
||||
case strings.HasPrefix(endpoint.URL, "udp://"):
|
||||
return EndpointTypeUDP
|
||||
case strings.HasPrefix(endpoint.URL, "icmp://"):
|
||||
return EndpointTypeICMP
|
||||
case strings.HasPrefix(endpoint.URL, "starttls://"):
|
||||
return EndpointTypeSTARTTLS
|
||||
case strings.HasPrefix(endpoint.URL, "tls://"):
|
||||
return EndpointTypeTLS
|
||||
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
|
||||
return EndpointTypeHTTP
|
||||
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
|
||||
return EndpointTypeWS
|
||||
case strings.HasPrefix(endpoint.URL, "ssh://"):
|
||||
return EndpointTypeSSH
|
||||
case e.DNSConfig != nil:
|
||||
return TypeDNS
|
||||
case strings.HasPrefix(e.URL, "tcp://"):
|
||||
return TypeTCP
|
||||
case strings.HasPrefix(e.URL, "sctp://"):
|
||||
return TypeSCTP
|
||||
case strings.HasPrefix(e.URL, "udp://"):
|
||||
return TypeUDP
|
||||
case strings.HasPrefix(e.URL, "icmp://"):
|
||||
return TypeICMP
|
||||
case strings.HasPrefix(e.URL, "starttls://"):
|
||||
return TypeSTARTTLS
|
||||
case strings.HasPrefix(e.URL, "tls://"):
|
||||
return TypeTLS
|
||||
case strings.HasPrefix(e.URL, "http://") || strings.HasPrefix(e.URL, "https://"):
|
||||
return TypeHTTP
|
||||
case strings.HasPrefix(e.URL, "ws://") || strings.HasPrefix(e.URL, "wss://"):
|
||||
return TypeWS
|
||||
case strings.HasPrefix(e.URL, "ssh://"):
|
||||
return TypeSSH
|
||||
default:
|
||||
return EndpointTypeUNKNOWN
|
||||
return TypeUNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
|
||||
func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
||||
if err := validateEndpointNameGroupAndAlerts(endpoint.Name, endpoint.Group, endpoint.Alerts); err != nil {
|
||||
func (e *Endpoint) ValidateAndSetDefaults() error {
|
||||
if err := validateEndpointNameGroupAndAlerts(e.Name, e.Group, e.Alerts); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(endpoint.URL) == 0 {
|
||||
if len(e.URL) == 0 {
|
||||
return ErrEndpointWithNoURL
|
||||
}
|
||||
if endpoint.ClientConfig == nil {
|
||||
endpoint.ClientConfig = client.GetDefaultConfig()
|
||||
if e.ClientConfig == nil {
|
||||
e.ClientConfig = client.GetDefaultConfig()
|
||||
} else {
|
||||
if err := endpoint.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
||||
if err := e.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if endpoint.UIConfig == nil {
|
||||
endpoint.UIConfig = ui.GetDefaultConfig()
|
||||
if e.UIConfig == nil {
|
||||
e.UIConfig = ui.GetDefaultConfig()
|
||||
} else {
|
||||
if err := endpoint.UIConfig.ValidateAndSetDefaults(); err != nil {
|
||||
if err := e.UIConfig.ValidateAndSetDefaults(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if endpoint.Interval == 0 {
|
||||
endpoint.Interval = 1 * time.Minute
|
||||
if e.Interval == 0 {
|
||||
e.Interval = 1 * time.Minute
|
||||
}
|
||||
if len(endpoint.Method) == 0 {
|
||||
endpoint.Method = http.MethodGet
|
||||
if len(e.Method) == 0 {
|
||||
e.Method = http.MethodGet
|
||||
}
|
||||
if len(endpoint.Headers) == 0 {
|
||||
endpoint.Headers = make(map[string]string)
|
||||
if len(e.Headers) == 0 {
|
||||
e.Headers = make(map[string]string)
|
||||
}
|
||||
// Automatically add user agent header if there isn't one specified in the endpoint configuration
|
||||
if _, userAgentHeaderExists := endpoint.Headers[UserAgentHeader]; !userAgentHeaderExists {
|
||||
endpoint.Headers[UserAgentHeader] = GatusUserAgent
|
||||
if _, userAgentHeaderExists := e.Headers[UserAgentHeader]; !userAgentHeaderExists {
|
||||
e.Headers[UserAgentHeader] = GatusUserAgent
|
||||
}
|
||||
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
|
||||
// and endpoint.GraphQL is set to true
|
||||
if _, contentTypeHeaderExists := endpoint.Headers[ContentTypeHeader]; !contentTypeHeaderExists && endpoint.GraphQL {
|
||||
endpoint.Headers[ContentTypeHeader] = "application/json"
|
||||
if _, contentTypeHeaderExists := e.Headers[ContentTypeHeader]; !contentTypeHeaderExists && e.GraphQL {
|
||||
e.Headers[ContentTypeHeader] = "application/json"
|
||||
}
|
||||
if len(endpoint.Conditions) == 0 {
|
||||
if len(e.Conditions) == 0 {
|
||||
return ErrEndpointWithNoCondition
|
||||
}
|
||||
for _, c := range endpoint.Conditions {
|
||||
if endpoint.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
|
||||
for _, c := range e.Conditions {
|
||||
if e.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
|
||||
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
|
||||
}
|
||||
if err := c.Validate(); err != nil {
|
||||
return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err)
|
||||
}
|
||||
}
|
||||
if endpoint.DNS != nil {
|
||||
return endpoint.DNS.validateAndSetDefault()
|
||||
if e.DNSConfig != nil {
|
||||
return e.DNSConfig.ValidateAndSetDefault()
|
||||
}
|
||||
if endpoint.SSH != nil {
|
||||
return endpoint.SSH.validate()
|
||||
if e.SSHConfig != nil {
|
||||
return e.SSHConfig.Validate()
|
||||
}
|
||||
if endpoint.Type() == EndpointTypeUNKNOWN {
|
||||
if e.Type() == TypeUNKNOWN {
|
||||
return ErrUnknownEndpointType
|
||||
}
|
||||
// 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 {
|
||||
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.
|
||||
func (endpoint *Endpoint) DisplayName() string {
|
||||
if len(endpoint.Group) > 0 {
|
||||
return endpoint.Group + "/" + endpoint.Name
|
||||
func (e *Endpoint) DisplayName() string {
|
||||
if len(e.Group) > 0 {
|
||||
return e.Group + "/" + e.Name
|
||||
}
|
||||
return endpoint.Name
|
||||
return e.Name
|
||||
}
|
||||
|
||||
// Key returns the unique key for the Endpoint
|
||||
func (endpoint *Endpoint) Key() string {
|
||||
return util.ConvertGroupAndEndpointNameToKey(endpoint.Group, endpoint.Name)
|
||||
func (e *Endpoint) Key() string {
|
||||
return ConvertGroupAndEndpointNameToKey(e.Group, e.Name)
|
||||
}
|
||||
|
||||
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
|
||||
// on configuration reload.
|
||||
// More context on https://github.com/TwiN/gatus/issues/536
|
||||
func (endpoint *Endpoint) Close() {
|
||||
if endpoint.Type() == EndpointTypeHTTP {
|
||||
client.GetHTTPClient(endpoint.ClientConfig).CloseIdleConnections()
|
||||
func (e *Endpoint) Close() {
|
||||
if e.Type() == TypeHTTP {
|
||||
client.GetHTTPClient(e.ClientConfig).CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
// 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{}}
|
||||
// Parse or extract hostname from URL
|
||||
if endpoint.DNS != nil {
|
||||
result.Hostname = strings.TrimSuffix(endpoint.URL, ":53")
|
||||
if e.DNSConfig != nil {
|
||||
result.Hostname = strings.TrimSuffix(e.URL, ":53")
|
||||
} else {
|
||||
urlObject, err := url.Parse(endpoint.URL)
|
||||
urlObject, err := url.Parse(e.URL)
|
||||
if err != nil {
|
||||
result.AddError(err.Error())
|
||||
} else {
|
||||
@ -263,11 +264,11 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
||||
}
|
||||
}
|
||||
// Retrieve IP if necessary
|
||||
if endpoint.needsToRetrieveIP() {
|
||||
endpoint.getIP(result)
|
||||
if e.needsToRetrieveIP() {
|
||||
e.getIP(result)
|
||||
}
|
||||
// Retrieve domain expiration if necessary
|
||||
if endpoint.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
|
||||
if e.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
|
||||
var err error
|
||||
if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil {
|
||||
result.AddError(err.Error())
|
||||
@ -275,37 +276,37 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
||||
}
|
||||
// Call the endpoint (if there's no errors)
|
||||
if len(result.Errors) == 0 {
|
||||
endpoint.call(result)
|
||||
e.call(result)
|
||||
} else {
|
||||
result.Success = false
|
||||
}
|
||||
// Evaluate the conditions
|
||||
for _, condition := range endpoint.Conditions {
|
||||
success := condition.evaluate(result, endpoint.UIConfig.DontResolveFailedConditions)
|
||||
for _, condition := range e.Conditions {
|
||||
success := condition.evaluate(result, e.UIConfig.DontResolveFailedConditions)
|
||||
if !success {
|
||||
result.Success = false
|
||||
}
|
||||
}
|
||||
result.Timestamp = time.Now()
|
||||
// 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 {
|
||||
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 {
|
||||
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
|
||||
}
|
||||
result.Hostname = ""
|
||||
}
|
||||
if endpoint.UIConfig.HideConditions {
|
||||
if e.UIConfig.HideConditions {
|
||||
result.ConditionResults = nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (endpoint *Endpoint) getIP(result *Result) {
|
||||
func (e *Endpoint) getIP(result *Result) {
|
||||
if ips, err := net.LookupIP(result.Hostname); err != nil {
|
||||
result.AddError(err.Error())
|
||||
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 response *http.Response
|
||||
var err error
|
||||
var certificate *x509.Certificate
|
||||
endpointType := endpoint.Type()
|
||||
if endpointType == EndpointTypeHTTP {
|
||||
request = endpoint.buildHTTPRequest()
|
||||
endpointType := e.Type()
|
||||
if endpointType == TypeHTTP {
|
||||
request = e.buildHTTPRequest()
|
||||
}
|
||||
startTime := time.Now()
|
||||
if endpointType == EndpointTypeDNS {
|
||||
endpoint.DNS.query(endpoint.URL, result)
|
||||
if endpointType == TypeDNS {
|
||||
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)
|
||||
} else if endpointType == EndpointTypeSTARTTLS || endpointType == EndpointTypeTLS {
|
||||
if endpointType == EndpointTypeSTARTTLS {
|
||||
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(endpoint.URL, "starttls://"), endpoint.ClientConfig)
|
||||
} else if endpointType == TypeSTARTTLS || endpointType == TypeTLS {
|
||||
if endpointType == TypeSTARTTLS {
|
||||
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(e.URL, "starttls://"), e.ClientConfig)
|
||||
} 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 {
|
||||
result.AddError(err.Error())
|
||||
@ -339,39 +344,39 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||
}
|
||||
result.Duration = time.Since(startTime)
|
||||
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
||||
} else if endpointType == EndpointTypeTCP {
|
||||
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig)
|
||||
} else if endpointType == TypeTCP {
|
||||
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(e.URL, "tcp://"), e.ClientConfig)
|
||||
result.Duration = time.Since(startTime)
|
||||
} else if endpointType == EndpointTypeUDP {
|
||||
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(endpoint.URL, "udp://"), endpoint.ClientConfig)
|
||||
} else if endpointType == TypeUDP {
|
||||
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(e.URL, "udp://"), e.ClientConfig)
|
||||
result.Duration = time.Since(startTime)
|
||||
} else if endpointType == EndpointTypeSCTP {
|
||||
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(endpoint.URL, "sctp://"), endpoint.ClientConfig)
|
||||
} else if endpointType == TypeSCTP {
|
||||
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(e.URL, "sctp://"), e.ClientConfig)
|
||||
result.Duration = time.Since(startTime)
|
||||
} else if endpointType == EndpointTypeICMP {
|
||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
|
||||
} else if endpointType == EndpointTypeWS {
|
||||
result.Connected, result.Body, err = client.QueryWebSocket(endpoint.URL, endpoint.Body, endpoint.ClientConfig)
|
||||
} else if endpointType == TypeICMP {
|
||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(e.URL, "icmp://"), e.ClientConfig)
|
||||
} else if endpointType == TypeWS {
|
||||
result.Connected, result.Body, err = client.QueryWebSocket(e.URL, e.Body, e.ClientConfig)
|
||||
if err != nil {
|
||||
result.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
result.Duration = time.Since(startTime)
|
||||
} else if endpointType == EndpointTypeSSH {
|
||||
} else if endpointType == TypeSSH {
|
||||
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 {
|
||||
result.AddError(err.Error())
|
||||
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 {
|
||||
result.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
result.Duration = time.Since(startTime)
|
||||
} else {
|
||||
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
||||
response, err = client.GetHTTPClient(e.ClientConfig).Do(request)
|
||||
result.Duration = time.Since(startTime)
|
||||
if err != nil {
|
||||
result.AddError(err.Error())
|
||||
@ -385,7 +390,7 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||
result.HTTPStatus = response.StatusCode
|
||||
result.Connected = response.StatusCode > 0
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
if endpoint.GraphQL {
|
||||
if e.GraphQL {
|
||||
graphQlBody := map[string]string{
|
||||
"query": endpoint.Body,
|
||||
"query": e.Body,
|
||||
}
|
||||
body, _ := json.Marshal(graphQlBody)
|
||||
bodyBuffer = bytes.NewBuffer(body)
|
||||
} else {
|
||||
bodyBuffer = bytes.NewBuffer([]byte(endpoint.Body))
|
||||
bodyBuffer = bytes.NewBuffer([]byte(e.Body))
|
||||
}
|
||||
request, _ := http.NewRequest(endpoint.Method, endpoint.URL, bodyBuffer)
|
||||
for k, v := range endpoint.Headers {
|
||||
request, _ := http.NewRequest(e.Method, e.URL, bodyBuffer)
|
||||
for k, v := range e.Headers {
|
||||
request.Header.Set(k, v)
|
||||
if k == HostHeader {
|
||||
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
|
||||
func (endpoint *Endpoint) needsToReadBody() bool {
|
||||
for _, condition := range endpoint.Conditions {
|
||||
func (e *Endpoint) needsToReadBody() bool {
|
||||
for _, condition := range e.Conditions {
|
||||
if condition.hasBodyPlaceholder() {
|
||||
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
|
||||
func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
|
||||
for _, condition := range endpoint.Conditions {
|
||||
func (e *Endpoint) needsToRetrieveDomainExpiration() bool {
|
||||
for _, condition := range e.Conditions {
|
||||
if condition.hasDomainExpirationPlaceholder() {
|
||||
return true
|
||||
}
|
||||
@ -436,8 +441,8 @@ func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
|
||||
}
|
||||
|
||||
// needsToRetrieveIP checks if there's any condition that requires an IP lookup
|
||||
func (endpoint *Endpoint) needsToRetrieveIP() bool {
|
||||
for _, condition := range endpoint.Conditions {
|
||||
func (e *Endpoint) needsToRetrieveIP() bool {
|
||||
for _, condition := range e.Conditions {
|
||||
if condition.hasIPPlaceholder() {
|
||||
return true
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -13,7 +13,9 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -279,105 +281,105 @@ func TestEndpoint_IsEnabled(t *testing.T) {
|
||||
func TestEndpoint_Type(t *testing.T) {
|
||||
type args struct {
|
||||
URL string
|
||||
DNS *DNS
|
||||
SSH *SSH
|
||||
DNS *dns.Config
|
||||
SSH *ssh.Config
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
want EndpointType
|
||||
want Type
|
||||
}{
|
||||
{
|
||||
args: args{
|
||||
URL: "8.8.8.8",
|
||||
DNS: &DNS{
|
||||
DNS: &dns.Config{
|
||||
QueryType: "A",
|
||||
QueryName: "example.com",
|
||||
},
|
||||
},
|
||||
want: EndpointTypeDNS,
|
||||
want: TypeDNS,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "tcp://127.0.0.1:6379",
|
||||
},
|
||||
want: EndpointTypeTCP,
|
||||
want: TypeTCP,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "icmp://example.com",
|
||||
},
|
||||
want: EndpointTypeICMP,
|
||||
want: TypeICMP,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "sctp://example.com",
|
||||
},
|
||||
want: EndpointTypeSCTP,
|
||||
want: TypeSCTP,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "udp://example.com",
|
||||
},
|
||||
want: EndpointTypeUDP,
|
||||
want: TypeUDP,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "starttls://smtp.gmail.com:587",
|
||||
},
|
||||
want: EndpointTypeSTARTTLS,
|
||||
want: TypeSTARTTLS,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "tls://example.com:443",
|
||||
},
|
||||
want: EndpointTypeTLS,
|
||||
want: TypeTLS,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "https://twin.sh/health",
|
||||
},
|
||||
want: EndpointTypeHTTP,
|
||||
want: TypeHTTP,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "wss://example.com/",
|
||||
},
|
||||
want: EndpointTypeWS,
|
||||
want: TypeWS,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "ws://example.com/",
|
||||
},
|
||||
want: EndpointTypeWS,
|
||||
want: TypeWS,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "ssh://example.com:22",
|
||||
SSH: &SSH{
|
||||
SSH: &ssh.Config{
|
||||
Username: "root",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
want: EndpointTypeSSH,
|
||||
want: TypeSSH,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "invalid://example.org",
|
||||
},
|
||||
want: EndpointTypeUNKNOWN,
|
||||
want: TypeUNKNOWN,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "no-scheme",
|
||||
},
|
||||
want: EndpointTypeUNKNOWN,
|
||||
want: TypeUNKNOWN,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.want), func(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
URL: tt.args.URL,
|
||||
DNS: tt.args.DNS,
|
||||
URL: tt.args.URL,
|
||||
DNSConfig: tt.args.DNS,
|
||||
}
|
||||
if got := endpoint.Type(); 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{
|
||||
Name: "dns-test",
|
||||
URL: "https://example.com",
|
||||
DNS: &DNS{
|
||||
DNSConfig: &dns.Config{
|
||||
QueryType: "A",
|
||||
QueryName: "example.com",
|
||||
},
|
||||
@ -487,7 +489,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@ -503,13 +505,13 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
||||
name: "fail when has no user",
|
||||
username: "",
|
||||
password: "password",
|
||||
expectedErr: ErrEndpointWithoutSSHUsername,
|
||||
expectedErr: ssh.ErrEndpointWithoutSSHUsername,
|
||||
},
|
||||
{
|
||||
name: "fail when has no password",
|
||||
username: "username",
|
||||
password: "",
|
||||
expectedErr: ErrEndpointWithoutSSHPassword,
|
||||
expectedErr: ssh.ErrEndpointWithoutSSHPassword,
|
||||
},
|
||||
{
|
||||
name: "success when all fields are set",
|
||||
@ -524,7 +526,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
||||
endpoint := &Endpoint{
|
||||
Name: "ssh-test",
|
||||
URL: "https://example.com",
|
||||
SSH: &SSH{
|
||||
SSHConfig: &ssh.Config{
|
||||
Username: scenario.username,
|
||||
Password: scenario.password,
|
||||
},
|
||||
@ -763,7 +765,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "example",
|
||||
URL: "8.8.8.8",
|
||||
DNS: &DNS{
|
||||
DNSConfig: &dns.Config{
|
||||
QueryType: "A",
|
||||
QueryName: "example.com.",
|
||||
},
|
||||
@ -786,7 +788,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||
tests := []struct {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
endpoint Endpoint
|
||||
conditions []Condition
|
||||
@ -797,9 +799,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||
endpoint: Endpoint{
|
||||
Name: "ssh-success",
|
||||
URL: "ssh://localhost",
|
||||
SSH: &SSH{
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
SSHConfig: &ssh.Config{
|
||||
Username: "scenario",
|
||||
Password: "scenario",
|
||||
},
|
||||
Body: "{ \"command\": \"uptime\" }",
|
||||
},
|
||||
@ -811,9 +813,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||
endpoint: Endpoint{
|
||||
Name: "ssh-failure",
|
||||
URL: "ssh://localhost",
|
||||
SSH: &SSH{
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
SSHConfig: &ssh.Config{
|
||||
Username: "scenario",
|
||||
Password: "scenario",
|
||||
},
|
||||
Body: "{ \"command\": \"uptime\" }",
|
||||
},
|
||||
@ -822,13 +824,13 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
test.endpoint.ValidateAndSetDefaults()
|
||||
test.endpoint.Conditions = test.conditions
|
||||
result := test.endpoint.EvaluateHealth()
|
||||
if result.Success != test.success {
|
||||
t.Errorf("Expected success to be %v, but was %v", test.success, result.Success)
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
scenario.endpoint.ValidateAndSetDefaults()
|
||||
scenario.endpoint.Conditions = scenario.conditions
|
||||
result := scenario.endpoint.EvaluateHealth()
|
||||
if result.Success != scenario.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
|
||||
type Event struct {
|
@ -1,6 +1,8 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewEventFromResult(t *testing.T) {
|
||||
if event := NewEventFromResult(&Result{Success: true}); event.Type != EventHealthy {
|
@ -1,10 +1,9 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/util"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -72,7 +71,7 @@ func (externalEndpoint *ExternalEndpoint) DisplayName() string {
|
||||
|
||||
// Key returns the unique key for the Endpoint
|
||||
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
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"testing"
|
@ -1,4 +1,4 @@
|
||||
package util
|
||||
package endpoint
|
||||
|
||||
import "strings"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package util
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"testing"
|
@ -1,4 +1,4 @@
|
||||
package util
|
||||
package endpoint
|
||||
|
||||
import "testing"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"time"
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"testing"
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -12,17 +12,17 @@ var (
|
||||
ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each SSH endpoint")
|
||||
)
|
||||
|
||||
type SSH struct {
|
||||
type Config struct {
|
||||
Username string `yaml:"username,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
}
|
||||
|
||||
// validate validates the endpoint
|
||||
func (s *SSH) validate() error {
|
||||
if len(s.Username) == 0 {
|
||||
// Validate the SSH configuration
|
||||
func (cfg *Config) Validate() error {
|
||||
if len(cfg.Username) == 0 {
|
||||
return ErrEndpointWithoutSSHUsername
|
||||
}
|
||||
if len(s.Password) == 0 {
|
||||
if len(cfg.Password) == 0 {
|
||||
return ErrEndpointWithoutSSHPassword
|
||||
}
|
||||
return nil
|
@ -1,4 +1,4 @@
|
||||
package core
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -6,20 +6,20 @@ import (
|
||||
)
|
||||
|
||||
func TestSSH_validate(t *testing.T) {
|
||||
ssh := &SSH{}
|
||||
if err := ssh.validate(); err == nil {
|
||||
cfg := &Config{}
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Error("expected an error")
|
||||
} else if !errors.Is(err, ErrEndpointWithoutSSHUsername) {
|
||||
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHUsername, err)
|
||||
}
|
||||
ssh.Username = "username"
|
||||
if err := ssh.validate(); err == nil {
|
||||
cfg.Username = "username"
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Error("expected an error")
|
||||
} else if !errors.Is(err, ErrEndpointWithoutSSHPassword) {
|
||||
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHPassword, err)
|
||||
}
|
||||
ssh.Password = "password"
|
||||
if err := ssh.validate(); err != nil {
|
||||
cfg.Password = "password"
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Errorf("expected no error, got '%v'", err)
|
||||
}
|
||||
}
|
@ -1,16 +1,14 @@
|
||||
package core
|
||||
package endpoint
|
||||
|
||||
import "github.com/TwiN/gatus/v5/util"
|
||||
|
||||
// EndpointStatus contains the evaluation Results of an Endpoint
|
||||
type EndpointStatus struct {
|
||||
// Status contains the evaluation Results of an Endpoint
|
||||
type Status struct {
|
||||
// Name of the endpoint
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// Group the endpoint is a part of. Used for grouping multiple endpoints together on the front end.
|
||||
Group string `json:"group,omitempty"`
|
||||
|
||||
// Key is the key representing the EndpointStatus
|
||||
// Key of the Endpoint
|
||||
Key string `json:"key"`
|
||||
|
||||
// Results is the list of endpoint evaluation results
|
||||
@ -27,12 +25,12 @@ type EndpointStatus struct {
|
||||
Uptime *Uptime `json:"-"`
|
||||
}
|
||||
|
||||
// NewEndpointStatus creates a new EndpointStatus
|
||||
func NewEndpointStatus(group, name string) *EndpointStatus {
|
||||
return &EndpointStatus{
|
||||
// NewStatus creates a new Status
|
||||
func NewStatus(group, name string) *Status {
|
||||
return &Status{
|
||||
Name: name,
|
||||
Group: group,
|
||||
Key: util.ConvertGroupAndEndpointNameToKey(group, name),
|
||||
Key: ConvertGroupAndEndpointNameToKey(group, name),
|
||||
Results: make([]*Result, 0),
|
||||
Events: make([]*Event, 0),
|
||||
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"
|
||||
|
||||
// Config is the UI configuration for core.Endpoint
|
||||
// Config is the UI configuration for endpoint.Endpoint
|
||||
type Config struct {
|
||||
// HideConditions whether to hide the condition results on the UI
|
||||
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
|
||||
// and some other statistics
|
@ -1 +0,0 @@
|
||||
TODO: move files from core to here.
|
@ -8,8 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"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/core"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
@ -19,7 +19,7 @@ func TestHandle(t *testing.T) {
|
||||
Address: "0.0.0.0",
|
||||
Port: rand.Intn(65534),
|
||||
},
|
||||
Endpoints: []*core.Endpoint{
|
||||
Endpoints: []*endpoint.Endpoint{
|
||||
{
|
||||
Name: "frontend",
|
||||
Group: "core",
|
||||
@ -64,7 +64,7 @@ func TestHandleTLS(t *testing.T) {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
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: "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
|
||||
var keys []string
|
||||
for _, endpoint := range cfg.Endpoints {
|
||||
keys = append(keys, endpoint.Key())
|
||||
for _, ep := range cfg.Endpoints {
|
||||
keys = append(keys, ep.Key())
|
||||
}
|
||||
for _, externalEndpoint := range cfg.ExternalEndpoints {
|
||||
keys = append(keys, externalEndpoint.Key())
|
||||
|
@ -3,7 +3,7 @@ package metrics
|
||||
import (
|
||||
"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/promauto"
|
||||
)
|
||||
@ -50,24 +50,24 @@ func initializePrometheusMetrics() {
|
||||
|
||||
// PublishMetricsForEndpoint publishes metrics for the given endpoint and its result.
|
||||
// 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 {
|
||||
initializePrometheusMetrics()
|
||||
initializedMetrics = true
|
||||
}
|
||||
endpointType := endpoint.Type()
|
||||
resultTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), strconv.FormatBool(result.Success)).Inc()
|
||||
resultDurationSeconds.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Set(result.Duration.Seconds())
|
||||
endpointType := ep.Type()
|
||||
resultTotal.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType), strconv.FormatBool(result.Success)).Inc()
|
||||
resultDurationSeconds.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType)).Set(result.Duration.Seconds())
|
||||
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 != "" {
|
||||
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 {
|
||||
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 {
|
||||
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"
|
||||
"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/testutil"
|
||||
)
|
||||
|
||||
func TestPublishMetricsForEndpoint(t *testing.T) {
|
||||
httpEndpoint := &core.Endpoint{Name: "http-ep-name", Group: "http-ep-group", URL: "https://example.org"}
|
||||
PublishMetricsForEndpoint(httpEndpoint, &core.Result{
|
||||
httpEndpoint := &endpoint.Endpoint{Name: "http-ep-name", Group: "http-ep-group", URL: "https://example.org"}
|
||||
PublishMetricsForEndpoint(httpEndpoint, &endpoint.Result{
|
||||
HTTPStatus: 200,
|
||||
Connected: true,
|
||||
Duration: 123 * time.Millisecond,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[STATUS] == 200", 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 {
|
||||
t.Errorf("Expected no errors but got: %v", err)
|
||||
}
|
||||
PublishMetricsForEndpoint(httpEndpoint, &core.Result{
|
||||
PublishMetricsForEndpoint(httpEndpoint, &endpoint.Result{
|
||||
HTTPStatus: 200,
|
||||
Connected: true,
|
||||
Duration: 125 * time.Millisecond,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[STATUS] == 200", Success: true},
|
||||
{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 {
|
||||
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",
|
||||
QueryName: "example.com.",
|
||||
}}
|
||||
PublishMetricsForEndpoint(dnsEndpoint, &core.Result{
|
||||
PublishMetricsForEndpoint(dnsEndpoint, &endpoint.Result{
|
||||
DNSRCode: "NOERROR",
|
||||
Connected: true,
|
||||
Duration: 50 * time.Millisecond,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{Condition: "[DNS_RCODE] == NOERROR", Success: true},
|
||||
},
|
||||
Success: true,
|
||||
|
@ -5,10 +5,9 @@ import (
|
||||
"sync"
|
||||
"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/paging"
|
||||
"github.com/TwiN/gatus/v5/util"
|
||||
"github.com/TwiN/gocache/v2"
|
||||
)
|
||||
|
||||
@ -30,13 +29,13 @@ func NewStore() (*Store, error) {
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// GetAllEndpointStatuses returns all monitored core.EndpointStatus
|
||||
// with a subset of core.Result defined by the page and pageSize parameters
|
||||
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*core.EndpointStatus, error) {
|
||||
// GetAllEndpointStatuses returns all monitored endpoint.Status
|
||||
// with a subset of endpoint.Result defined by the page and pageSize parameters
|
||||
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error) {
|
||||
endpointStatuses := s.cache.GetAll()
|
||||
pagedEndpointStatuses := make([]*core.EndpointStatus, 0, len(endpointStatuses))
|
||||
pagedEndpointStatuses := make([]*endpoint.Status, 0, len(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 {
|
||||
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
|
||||
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) {
|
||||
return s.GetEndpointStatusByKey(util.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
|
||||
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
|
||||
return s.GetEndpointStatusByKey(endpoint.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if endpointStatus == nil {
|
||||
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
|
||||
@ -64,7 +63,7 @@ func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
||||
return 0, common.ErrInvalidTimeRange
|
||||
}
|
||||
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
|
||||
}
|
||||
successfulExecutions := uint64(0)
|
||||
@ -72,7 +71,7 @@ func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
||||
current := from
|
||||
for to.Sub(current) >= 0 {
|
||||
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 {
|
||||
current = current.Add(time.Hour)
|
||||
continue
|
||||
@ -93,14 +92,14 @@ func (s *Store) GetAverageResponseTimeByKey(key string, from, to time.Time) (int
|
||||
return 0, common.ErrInvalidTimeRange
|
||||
}
|
||||
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
|
||||
}
|
||||
current := from
|
||||
var totalExecutions, totalResponseTime uint64
|
||||
for to.Sub(current) >= 0 {
|
||||
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 {
|
||||
current = current.Add(time.Hour)
|
||||
continue
|
||||
@ -121,14 +120,14 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time
|
||||
return nil, common.ErrInvalidTimeRange
|
||||
}
|
||||
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
|
||||
}
|
||||
hourlyAverageResponseTimes := make(map[int64]int)
|
||||
current := from
|
||||
for to.Sub(current) >= 0 {
|
||||
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 {
|
||||
current = current.Add(time.Hour)
|
||||
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
|
||||
func (s *Store) Insert(endpoint *core.Endpoint, result *core.Result) error {
|
||||
key := endpoint.Key()
|
||||
func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error {
|
||||
key := ep.Key()
|
||||
s.Lock()
|
||||
status, exists := s.cache.Get(key)
|
||||
if !exists {
|
||||
status = core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
||||
status.(*core.EndpointStatus).Events = append(status.(*core.EndpointStatus).Events, &core.Event{
|
||||
Type: core.EventStart,
|
||||
status = endpoint.NewStatus(ep.Group, ep.Name)
|
||||
status.(*endpoint.Status).Events = append(status.(*endpoint.Status).Events, &endpoint.Event{
|
||||
Type: endpoint.EventStart,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
AddResult(status.(*core.EndpointStatus), result)
|
||||
AddResult(status.(*endpoint.Status), result)
|
||||
s.cache.Set(key, status)
|
||||
s.Unlock()
|
||||
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 {
|
||||
var keysToDelete []string
|
||||
for _, existingKey := range s.cache.GetKeysByPattern("*", 0) {
|
||||
|
@ -4,30 +4,30 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||
)
|
||||
|
||||
var (
|
||||
firstCondition = core.Condition("[STATUS] == 200")
|
||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||
firstCondition = endpoint.Condition("[STATUS] == 200")
|
||||
secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
|
||||
thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||
|
||||
now = time.Now()
|
||||
|
||||
testEndpoint = core.Endpoint{
|
||||
testEndpoint = endpoint.Endpoint{
|
||||
Name: "name",
|
||||
Group: "group",
|
||||
URL: "https://example.org/what/ever",
|
||||
Method: "GET",
|
||||
Body: "body",
|
||||
Interval: 30 * time.Second,
|
||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Alerts: nil,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
}
|
||||
testSuccessfulResult = core.Result{
|
||||
testSuccessfulResult = endpoint.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
@ -37,7 +37,7 @@ var (
|
||||
Timestamp: now,
|
||||
Duration: 150 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
@ -52,7 +52,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
testUnsuccessfulResult = core.Result{
|
||||
testUnsuccessfulResult = endpoint.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
@ -62,7 +62,7 @@ var (
|
||||
Timestamp: now,
|
||||
Duration: 750 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
|
@ -3,7 +3,7 @@ package memory
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -13,14 +13,14 @@ const (
|
||||
|
||||
// processUptimeAfterResult processes the result by extracting the relevant from the result and recalculating the uptime
|
||||
// if necessary
|
||||
func processUptimeAfterResult(uptime *core.Uptime, result *core.Result) {
|
||||
func processUptimeAfterResult(uptime *endpoint.Uptime, result *endpoint.Result) {
|
||||
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()
|
||||
hourlyStats, _ := uptime.HourlyStatistics[unixTimestampFlooredAtHour]
|
||||
if hourlyStats == nil {
|
||||
hourlyStats = &core.HourlyUptimeStatistics{}
|
||||
hourlyStats = &endpoint.HourlyUptimeStatistics{}
|
||||
uptime.HourlyStatistics[unixTimestampFlooredAtHour] = hourlyStats
|
||||
}
|
||||
if result.Success {
|
||||
|
@ -4,17 +4,17 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
)
|
||||
|
||||
func BenchmarkProcessUptimeAfterResult(b *testing.B) {
|
||||
uptime := core.NewUptime()
|
||||
uptime := endpoint.NewUptime()
|
||||
now := time.Now()
|
||||
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
||||
// Start 12000 days ago
|
||||
timestamp := now.Add(-12000 * 24 * time.Hour)
|
||||
for n := 0; n < b.N; n++ {
|
||||
processUptimeAfterResult(uptime, &core.Result{
|
||||
processUptimeAfterResult(uptime, &endpoint.Result{
|
||||
Duration: 18 * time.Millisecond,
|
||||
Success: n%15 == 0,
|
||||
Timestamp: timestamp,
|
||||
|
@ -4,53 +4,53 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
)
|
||||
|
||||
func TestProcessUptimeAfterResult(t *testing.T) {
|
||||
endpoint := &core.Endpoint{Name: "name", Group: "group"}
|
||||
status := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
||||
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||
status := endpoint.NewStatus(ep.Group, ep.Name)
|
||||
uptime := status.Uptime
|
||||
|
||||
now := time.Now()
|
||||
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, &core.Result{Timestamp: now.Add(-12 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-24 * 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)
|
||||
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)
|
||||
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)
|
||||
|
||||
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, &core.Result{Timestamp: now.Add(-119 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-118 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-117 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-10 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &core.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(-120 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-119 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-118 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-117 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-10 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})
|
||||
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-30 * time.Minute), Success: true})
|
||||
processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-25 * time.Minute), Success: true})
|
||||
}
|
||||
|
||||
func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
|
||||
endpoint := &core.Endpoint{Name: "name", Group: "group"}
|
||||
status := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
||||
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||
status := endpoint.NewStatus(ep.Group, ep.Name)
|
||||
now := time.Now()
|
||||
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
||||
// Start 12 days ago
|
||||
timestamp := now.Add(-12 * 24 * time.Hour)
|
||||
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 {
|
||||
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 {
|
||||
t.Error("TotalExecutionsResponseTime should've been", expectedTotalExecutionsResponseTime, "got", hourlyUptimeStatistics.TotalExecutionsResponseTime)
|
||||
}
|
||||
|
@ -1,31 +1,31 @@
|
||||
package memory
|
||||
|
||||
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/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
|
||||
func ShallowCopyEndpointStatus(ss *core.EndpointStatus, params *paging.EndpointStatusParams) *core.EndpointStatus {
|
||||
shallowCopy := &core.EndpointStatus{
|
||||
func ShallowCopyEndpointStatus(ss *endpoint.Status, params *paging.EndpointStatusParams) *endpoint.Status {
|
||||
shallowCopy := &endpoint.Status{
|
||||
Name: ss.Name,
|
||||
Group: ss.Group,
|
||||
Key: ss.Key,
|
||||
Uptime: core.NewUptime(),
|
||||
Uptime: endpoint.NewUptime(),
|
||||
}
|
||||
numberOfResults := len(ss.Results)
|
||||
resultsStart, resultsEnd := getStartAndEndIndex(numberOfResults, params.ResultsPage, params.ResultsPageSize)
|
||||
if resultsStart < 0 || resultsEnd < 0 {
|
||||
shallowCopy.Results = []*core.Result{}
|
||||
shallowCopy.Results = []*endpoint.Result{}
|
||||
} else {
|
||||
shallowCopy.Results = ss.Results[resultsStart:resultsEnd]
|
||||
}
|
||||
numberOfEvents := len(ss.Events)
|
||||
eventsStart, eventsEnd := getStartAndEndIndex(numberOfEvents, params.EventsPage, params.EventsPageSize)
|
||||
if eventsStart < 0 || eventsEnd < 0 {
|
||||
shallowCopy.Events = []*core.Event{}
|
||||
shallowCopy.Events = []*endpoint.Event{}
|
||||
} else {
|
||||
shallowCopy.Events = ss.Events[eventsStart:eventsEnd]
|
||||
}
|
||||
@ -49,16 +49,16 @@ func getStartAndEndIndex(numberOfResults int, page, pageSize int) (int, int) {
|
||||
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
|
||||
func AddResult(ss *core.EndpointStatus, result *core.Result) {
|
||||
func AddResult(ss *endpoint.Status, result *endpoint.Result) {
|
||||
if ss == nil {
|
||||
return
|
||||
}
|
||||
if len(ss.Results) > 0 {
|
||||
// Check if there's any change since the last result
|
||||
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 {
|
||||
// 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
|
||||
@ -68,7 +68,7 @@ func AddResult(ss *core.EndpointStatus, result *core.Result) {
|
||||
}
|
||||
} else {
|
||||
// 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)
|
||||
if len(ss.Results) > common.MaximumNumberOfResults {
|
||||
|
@ -3,14 +3,14 @@ package memory
|
||||
import (
|
||||
"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/paging"
|
||||
)
|
||||
|
||||
func BenchmarkShallowCopyEndpointStatus(b *testing.B) {
|
||||
endpoint := &testEndpoint
|
||||
status := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
||||
ep := &testEndpoint
|
||||
status := endpoint.NewStatus(ep.Group, ep.Name)
|
||||
for i := 0; i < common.MaximumNumberOfResults; i++ {
|
||||
AddResult(status, &testSuccessfulResult)
|
||||
}
|
||||
|
@ -4,16 +4,16 @@ import (
|
||||
"testing"
|
||||
"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/paging"
|
||||
)
|
||||
|
||||
func TestAddResult(t *testing.T) {
|
||||
endpoint := &core.Endpoint{Name: "name", Group: "group"}
|
||||
endpointStatus := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
||||
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
// Try to add nil endpointStatus
|
||||
AddResult(nil, &core.Result{Timestamp: time.Now()})
|
||||
AddResult(nil, &endpoint.Result{Timestamp: time.Now()})
|
||||
}
|
||||
|
||||
func TestShallowCopyEndpointStatus(t *testing.T) {
|
||||
endpoint := &core.Endpoint{Name: "name", Group: "group"}
|
||||
endpointStatus := core.NewEndpointStatus(endpoint.Group, endpoint.Name)
|
||||
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
|
||||
ts := time.Now().Add(-25 * time.Hour)
|
||||
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)
|
||||
}
|
||||
if len(ShallowCopyEndpointStatus(endpointStatus, paging.NewEndpointStatusParams().WithResults(-1, -1)).Results) != 0 {
|
||||
|
@ -9,10 +9,9 @@ import (
|
||||
"strings"
|
||||
"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/paging"
|
||||
"github.com/TwiN/gatus/v5/util"
|
||||
"github.com/TwiN/gocache/v2"
|
||||
_ "github.com/lib/pq"
|
||||
_ "modernc.org/sqlite"
|
||||
@ -100,9 +99,9 @@ func (s *Store) createSchema() error {
|
||||
return s.createPostgresSchema()
|
||||
}
|
||||
|
||||
// GetAllEndpointStatuses returns all monitored core.EndpointStatus
|
||||
// with a subset of core.Result defined by the page and pageSize parameters
|
||||
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*core.EndpointStatus, error) {
|
||||
// GetAllEndpointStatuses returns all monitored endpoint.Status
|
||||
// with a subset of endpoint.Result defined by the page and pageSize parameters
|
||||
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error) {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -112,7 +111,7 @@ func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
endpointStatuses := make([]*core.EndpointStatus, 0, len(keys))
|
||||
endpointStatuses := make([]*endpoint.Status, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
endpointStatus, err := s.getEndpointStatusByKey(tx, key, params)
|
||||
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
|
||||
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) {
|
||||
return s.GetEndpointStatusByKey(util.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
|
||||
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
|
||||
return s.GetEndpointStatusByKey(endpoint.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
|
||||
}
|
||||
|
||||
// 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()
|
||||
if err != nil {
|
||||
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
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointID, err := s.getEndpointID(tx, endpoint)
|
||||
endpointID, err := s.getEndpointID(tx, ep)
|
||||
if err != nil {
|
||||
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||
// 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()
|
||||
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
|
||||
}
|
||||
} else {
|
||||
_ = 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
|
||||
}
|
||||
}
|
||||
// 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:
|
||||
// 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
|
||||
// based on result.Success.
|
||||
// 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)
|
||||
if err != nil {
|
||||
// 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 {
|
||||
// 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{
|
||||
Type: core.EventStart,
|
||||
err = s.insertEndpointEvent(tx, endpointID, &endpoint.Event{
|
||||
Type: endpoint.EventStart,
|
||||
Timestamp: result.Timestamp.Add(-50 * time.Millisecond),
|
||||
})
|
||||
if err != nil {
|
||||
// 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 {
|
||||
// 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 {
|
||||
// Get the success value of the previous result
|
||||
var lastResultSuccess bool
|
||||
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 {
|
||||
// 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
|
||||
// that the endpoint either went from Healthy to Unhealthy or Unhealthy -> Healthy, therefore, we'll add
|
||||
// an event to mark the change in state
|
||||
if lastResultSuccess != result.Success {
|
||||
event := core.NewEventFromResult(result)
|
||||
event := endpoint.NewEventFromResult(result)
|
||||
if err = s.insertEndpointEvent(tx, endpointID, event); err != nil {
|
||||
// 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)
|
||||
if numberOfEvents > eventsCleanUpThreshold {
|
||||
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.
|
||||
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
|
||||
return err
|
||||
}
|
||||
// Clean up old results
|
||||
numberOfResults, err := s.getNumberOfResultsByEndpointID(tx, endpointID)
|
||||
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 {
|
||||
if numberOfResults > resultsCleanUpThreshold {
|
||||
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.
|
||||
// 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 {
|
||||
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
|
||||
ageOfOldestUptimeEntry, err := s.getAgeOfOldestEndpointUptimeEntry(tx, endpointID)
|
||||
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 {
|
||||
if ageOfOldestUptimeEntry > uptimeCleanUpThreshold {
|
||||
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 {
|
||||
cacheKeysToRefresh := s.writeThroughCache.GetKeysByPattern(endpoint.Key()+"*", 0)
|
||||
cacheKeysToRefresh := s.writeThroughCache.GetKeysByPattern(ep.Key()+"*", 0)
|
||||
for _, cacheKey := range cacheKeysToRefresh {
|
||||
s.writeThroughCache.Delete(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
|
||||
func (s *Store) insertEndpoint(tx *sql.Tx, endpoint *core.Endpoint) (int64, error) {
|
||||
//log.Printf("[sql.insertEndpoint] Inserting endpoint with group=%s and name=%s", endpoint.Group, endpoint.Name)
|
||||
func (s *Store) insertEndpoint(tx *sql.Tx, ep *endpoint.Endpoint) (int64, error) {
|
||||
//log.Printf("[sql.insertEndpoint] Inserting endpoint with group=%s and name=%s", ep.Group, ep.Name)
|
||||
var id int64
|
||||
err := tx.QueryRow(
|
||||
"INSERT INTO endpoints (endpoint_key, endpoint_name, endpoint_group) VALUES ($1, $2, $3) RETURNING endpoint_id",
|
||||
endpoint.Key(),
|
||||
endpoint.Name,
|
||||
endpoint.Group,
|
||||
ep.Key(),
|
||||
ep.Name,
|
||||
ep.Group,
|
||||
).Scan(&id)
|
||||
if err != nil {
|
||||
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
|
||||
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(
|
||||
"INSERT INTO endpoint_events (endpoint_id, event_type, event_timestamp) VALUES ($1, $2, $3)",
|
||||
endpointID,
|
||||
@ -435,7 +434,7 @@ func (s *Store) insertEndpointEvent(tx *sql.Tx, endpointID int64, event *core.Ev
|
||||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
for _, cr := range conditionResults {
|
||||
_, 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
|
||||
}
|
||||
|
||||
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()
|
||||
var successfulExecutions int
|
||||
if result.Success {
|
||||
@ -514,12 +513,12 @@ func (s *Store) getAllEndpointKeys(tx *sql.Tx) (keys []string, err error) {
|
||||
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
|
||||
if s.writeThroughCache != nil {
|
||||
cacheKey = generateCacheKey(key, parameters)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -528,7 +527,7 @@ func (s *Store) getEndpointStatusByKey(tx *sql.Tx, key string, parameters *pagin
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpointStatus := core.NewEndpointStatus(group, endpointName)
|
||||
endpointStatus := endpoint.NewStatus(group, endpointName)
|
||||
if parameters.EventsPageSize > 0 {
|
||||
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())
|
||||
@ -564,7 +563,7 @@ func (s *Store) getEndpointIDGroupAndNameByKey(tx *sql.Tx, key string) (id int64
|
||||
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(
|
||||
`
|
||||
SELECT event_type, event_timestamp
|
||||
@ -581,14 +580,14 @@ func (s *Store) getEndpointEventsByEndpointID(tx *sql.Tx, endpointID int64, page
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
event := &core.Event{}
|
||||
event := &endpoint.Event{}
|
||||
_ = rows.Scan(&event.Type, &event.Timestamp)
|
||||
events = append(events, event)
|
||||
}
|
||||
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(
|
||||
`
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
idResultMap := make(map[int64]*core.Result)
|
||||
idResultMap := make(map[int64]*endpoint.Result)
|
||||
for rows.Next() {
|
||||
result := &core.Result{}
|
||||
result := &endpoint.Result{}
|
||||
var id int64
|
||||
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)
|
||||
@ -618,7 +617,7 @@ func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, pag
|
||||
result.Errors = strings.Split(joinedErrors, arraySeparator)
|
||||
}
|
||||
// This is faster than using a subselect
|
||||
results = append([]*core.Result{result}, results...)
|
||||
results = append([]*endpoint.Result{result}, results...)
|
||||
idResultMap[id] = result
|
||||
}
|
||||
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
|
||||
for rows.Next() {
|
||||
conditionResult := &core.ConditionResult{}
|
||||
conditionResult := &endpoint.ConditionResult{}
|
||||
var endpointResultID int64
|
||||
if err = rows.Scan(&endpointResultID, &conditionResult.Condition, &conditionResult.Success); err != nil {
|
||||
return
|
||||
@ -734,9 +733,9 @@ func (s *Store) getEndpointHourlyAverageResponseTimes(tx *sql.Tx, endpointID int
|
||||
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
|
||||
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 errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, common.ErrEndpointNotFound
|
||||
|
@ -4,31 +4,31 @@ import (
|
||||
"testing"
|
||||
"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/paging"
|
||||
)
|
||||
|
||||
var (
|
||||
firstCondition = core.Condition("[STATUS] == 200")
|
||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||
firstCondition = endpoint.Condition("[STATUS] == 200")
|
||||
secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
|
||||
thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||
|
||||
now = time.Now()
|
||||
|
||||
testEndpoint = core.Endpoint{
|
||||
testEndpoint = endpoint.Endpoint{
|
||||
Name: "name",
|
||||
Group: "group",
|
||||
URL: "https://example.org/what/ever",
|
||||
Method: "GET",
|
||||
Body: "body",
|
||||
Interval: 30 * time.Second,
|
||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Alerts: nil,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
}
|
||||
testSuccessfulResult = core.Result{
|
||||
testSuccessfulResult = endpoint.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
@ -38,7 +38,7 @@ var (
|
||||
Timestamp: now,
|
||||
Duration: 150 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
@ -53,7 +53,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
testUnsuccessfulResult = core.Result{
|
||||
testUnsuccessfulResult = endpoint.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
@ -63,7 +63,7 @@ var (
|
||||
Timestamp: now,
|
||||
Duration: 750 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
ConditionResults: []*endpoint.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
@ -100,7 +100,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
|
||||
now := time.Now().Truncate(time.Hour)
|
||||
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()
|
||||
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
|
||||
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()
|
||||
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
|
||||
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()
|
||||
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
|
||||
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()
|
||||
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
|
||||
// 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()
|
||||
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
|
||||
@ -313,7 +313,7 @@ func TestStore_InvalidTransaction(t *testing.T) {
|
||||
if _, err := store.insertEndpoint(tx, &testEndpoint); err == nil {
|
||||
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")
|
||||
}
|
||||
if err := store.insertEndpointResult(tx, 1, &testSuccessfulResult); err == nil {
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"log"
|
||||
"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/store/common/paging"
|
||||
"github.com/TwiN/gatus/v5/storage/store/memory"
|
||||
@ -14,15 +14,15 @@ import (
|
||||
|
||||
// Store is the interface that each store should implement
|
||||
type Store interface {
|
||||
// GetAllEndpointStatuses returns the JSON encoding of all monitored core.EndpointStatus
|
||||
// with a subset of core.Result defined by the page and pageSize parameters
|
||||
GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*core.EndpointStatus, error)
|
||||
// GetAllEndpointStatuses returns the JSON encoding of all monitored endpoint.Status
|
||||
// with a subset of endpoint.Result defined by the page and pageSize parameters
|
||||
GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error)
|
||||
|
||||
// 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(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(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)
|
||||
|
||||
// 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
|
||||
DeleteAllEndpointStatusesNotInKeys(keys []string) int
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"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/memory"
|
||||
"github.com/TwiN/gatus/v5/storage/store/sql"
|
||||
@ -53,11 +53,11 @@ func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) {
|
||||
for _, numberOfEndpointsToCreate := range numberOfEndpoints {
|
||||
// Create endpoints and insert results
|
||||
for i := 0; i < numberOfEndpointsToCreate; i++ {
|
||||
endpoint := testEndpoint
|
||||
endpoint.Name = "endpoint" + strconv.Itoa(i)
|
||||
ep := testEndpoint
|
||||
ep.Name = "endpoint" + strconv.Itoa(i)
|
||||
// Insert 20 results for each endpoint
|
||||
for j := 0; j < 20; j++ {
|
||||
scenario.Store.Insert(&endpoint, &testSuccessfulResult)
|
||||
scenario.Store.Insert(&ep, &testSuccessfulResult)
|
||||
}
|
||||
}
|
||||
// Run the scenarios
|
||||
@ -123,7 +123,7 @@ func BenchmarkStore_Insert(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
n := 0
|
||||
for pb.Next() {
|
||||
var result core.Result
|
||||
var result endpoint.Result
|
||||
if n%10 == 0 {
|
||||
result = testUnsuccessfulResult
|
||||
} else {
|
||||
@ -136,7 +136,7 @@ func BenchmarkStore_Insert(b *testing.B) {
|
||||
})
|
||||
} else {
|
||||
for n := 0; n < b.N; n++ {
|
||||
var result core.Result
|
||||
var result endpoint.Result
|
||||
if n%10 == 0 {
|
||||
result = testUnsuccessfulResult
|
||||
} else {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user