1
0
Fork 0
mirror of https://github.com/TwiN/gatus.git synced 2024-12-14 11:58:04 +00:00

refactor: Break core package into multiple packages under config/endpoint (#759)

* refactor: Partially break core package into dns, result and ssh packages

* refactor: Move core package to config/endpoint

* refactor: Fix warning about overlapping imported package name with endpoint variable

* refactor: Rename EndpointStatus to Status

* refactor: Merge result pkg back into endpoint pkg, because it makes more sense

* refactor: Rename parameter r to result in Condition.evaluate

* refactor: Rename parameter r to result

* refactor: Revert accidental change to endpoint.TypeDNS

* refactor: Rename parameter r to result

* refactor: Merge util package into endpoint package

* refactor: Rename parameter r to result
This commit is contained in:
TwiN 2024-05-09 22:56:16 -04:00 committed by GitHub
parent 4397dcb5fc
commit 9d151fcdb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
104 changed files with 1216 additions and 1211 deletions

View file

@ -10,7 +10,7 @@ var (
ErrAlertWithInvalidDescription = errors.New("alert description must not have \" or \\") ErrAlertWithInvalidDescription = errors.New("alert description must not have \" or \\")
) )
// Alert is a core.Endpoint's alert configuration // Alert is a endpoint.Endpoint's alert configuration
type Alert struct { type Alert struct {
// Type of alert (required) // Type of alert (required)
Type Type `yaml:"type"` Type Type `yaml:"type"`

View file

@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
@ -57,14 +57,14 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
sess, err := provider.createSession() sess, err := provider.createSession()
if err != nil { if err != nil {
return err return err
} }
svc := ses.New(sess) svc := ses.New(sess)
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved) subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
emails := strings.Split(provider.getToForGroup(endpoint.Group), ",") emails := strings.Split(provider.getToForGroup(ep.Group), ",")
input := &ses.SendEmailInput{ input := &ses.SendEmailInput{
Destination: &ses.Destination{ Destination: &ses.Destination{
@ -110,14 +110,14 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
} }
// buildMessageSubjectAndBody builds the message subject and body // buildMessageSubjectAndBody builds the message subject and body
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) { func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
var subject, message string var subject, message string
if resolved { if resolved {
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName()) subject = fmt.Sprintf("[%s] Alert resolved", ep.DisplayName())
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
} else { } else {
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName()) subject = fmt.Sprintf("[%s] Alert triggered", ep.DisplayName())
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
} }
var formattedConditionResults string var formattedConditionResults string
if len(result.ConditionResults) > 0 { if len(result.ConditionResults) > 0 {

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
func TestAlertDefaultProvider_IsValid(t *testing.T) { func TestAlertDefaultProvider_IsValid(t *testing.T) {
@ -95,10 +95,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
subject, body := scenario.Provider.buildMessageSubjectAndBody( subject, body := scenario.Provider.buildMessageSubjectAndBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request // AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
@ -50,16 +50,16 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri
return status return status
} }
func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *alert.Alert, resolved bool) *http.Request { func (provider *AlertProvider) buildHTTPRequest(ep *endpoint.Endpoint, alert *alert.Alert, resolved bool) *http.Request {
body, url, method := provider.Body, provider.URL, provider.Method body, url, method := provider.Body, provider.URL, provider.Method
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription()) body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription()) url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpoint.Name) body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", ep.Name)
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", endpoint.Name) url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", ep.Name)
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", endpoint.Group) body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", ep.Group)
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", endpoint.Group) url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", ep.Group)
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", endpoint.URL) body = strings.ReplaceAll(body, "[ENDPOINT_URL]", ep.URL)
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", endpoint.URL) url = strings.ReplaceAll(url, "[ENDPOINT_URL]", ep.URL)
if resolved { if resolved {
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
@ -78,8 +78,8 @@ func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *
return request return request
} }
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
request := provider.buildHTTPRequest(endpoint, alert, resolved) request := provider.buildHTTPRequest(ep, alert, resolved)
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request) response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
if err != nil { if err != nil {
return err return err

View file

@ -8,7 +8,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -90,10 +90,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -138,7 +138,7 @@ func TestAlertProvider_buildHTTPRequest(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) { t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
request := customAlertProvider.buildHTTPRequest( request := customAlertProvider.buildHTTPRequest(
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"}, &endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
&alert.Alert{Description: &alertDescription}, &alert.Alert{Description: &alertDescription},
scenario.Resolved, scenario.Resolved,
) )
@ -188,7 +188,7 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) { t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
request := customAlertProvider.buildHTTPRequest( request := customAlertProvider.buildHTTPRequest(
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"}, &endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
&alert.Alert{Description: &alertDescription}, &alert.Alert{Description: &alertDescription},
scenario.Resolved, scenario.Resolved,
) )

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the configuration necessary for sending an alert using Discord // AlertProvider is the configuration necessary for sending an alert using Discord
@ -47,9 +47,9 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer) request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
if err != nil { if err != nil {
return err return err
} }
@ -85,14 +85,14 @@ type Field struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string var message string
var colorCode int var colorCode int
if resolved { if resolved {
message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
colorCode = 3066993 colorCode = 3066993
} else { } else {
message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
colorCode = 15158332 colorCode = 15158332
} }
var formattedConditionResults string var formattedConditionResults string

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -127,10 +127,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -191,18 +191,18 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
var conditionResults []*core.ConditionResult var conditionResults []*endpoint.ConditionResult
if !scenario.NoConditions { if !scenario.NoConditions {
conditionResults = []*core.ConditionResult{ conditionResults = []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
{Condition: "[BODY] != \"\"", Success: scenario.Resolved}, {Condition: "[BODY] != \"\"", Success: scenario.Resolved},
} }
} }
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: conditionResults, ConditionResults: conditionResults,
}, },
scenario.Resolved, scenario.Resolved,

View file

@ -8,7 +8,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
gomail "gopkg.in/mail.v2" gomail "gopkg.in/mail.v2"
) )
@ -53,17 +53,17 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
var username string var username string
if len(provider.Username) > 0 { if len(provider.Username) > 0 {
username = provider.Username username = provider.Username
} else { } else {
username = provider.From username = provider.From
} }
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved) subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
m := gomail.NewMessage() m := gomail.NewMessage()
m.SetHeader("From", provider.From) m.SetHeader("From", provider.From)
m.SetHeader("To", strings.Split(provider.getToForGroup(endpoint.Group), ",")...) m.SetHeader("To", strings.Split(provider.getToForGroup(ep.Group), ",")...)
m.SetHeader("Subject", subject) m.SetHeader("Subject", subject)
m.SetBody("text/plain", body) m.SetBody("text/plain", body)
var d *gomail.Dialer var d *gomail.Dialer
@ -87,14 +87,14 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
} }
// buildMessageSubjectAndBody builds the message subject and body // buildMessageSubjectAndBody builds the message subject and body
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) { func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
var subject, message string var subject, message string
if resolved { if resolved {
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName()) subject = fmt.Sprintf("[%s] Alert resolved", ep.DisplayName())
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
} else { } else {
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName()) subject = fmt.Sprintf("[%s] Alert triggered", ep.DisplayName())
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
} }
var formattedConditionResults string var formattedConditionResults string
if len(result.ConditionResults) > 0 { if len(result.ConditionResults) > 0 {

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
func TestAlertDefaultProvider_IsValid(t *testing.T) { func TestAlertDefaultProvider_IsValid(t *testing.T) {
@ -97,10 +97,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
subject, body := scenario.Provider.buildMessageSubjectAndBody( subject, body := scenario.Provider.buildMessageSubjectAndBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -8,7 +8,7 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/google/go-github/v48/github" "github.com/google/go-github/v48/github"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -70,12 +70,12 @@ func (provider *AlertProvider) IsValid() bool {
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false, // Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
// or closes the relevant issue(s) if the resolved parameter passed is true. // or closes the relevant issue(s) if the resolved parameter passed is true.
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
title := "alert(gatus): " + endpoint.DisplayName() title := "alert(gatus): " + ep.DisplayName()
if !resolved { if !resolved {
_, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{ _, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
Title: github.String(title), Title: github.String(title),
Body: github.String(provider.buildIssueBody(endpoint, alert, result)), Body: github.String(provider.buildIssueBody(ep, alert, result)),
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to create issue: %w", err) return fmt.Errorf("failed to create issue: %w", err)
@ -104,7 +104,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
} }
// buildIssueBody builds the body of the issue // buildIssueBody builds the body of the issue
func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result) string { func (provider *AlertProvider) buildIssueBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result) string {
var formattedConditionResults string var formattedConditionResults string
if len(result.ConditionResults) > 0 { if len(result.ConditionResults) > 0 {
formattedConditionResults = "\n\n## Condition results\n" formattedConditionResults = "\n\n## Condition results\n"
@ -122,7 +122,7 @@ func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *al
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = ":\n> " + alertDescription description = ":\n> " + alertDescription
} }
message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
return message + description + formattedConditionResults return message + description + formattedConditionResults
} }

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
"github.com/google/go-github/v48/github" "github.com/google/go-github/v48/github"
) )
@ -85,10 +85,10 @@ func TestAlertProvider_Send(t *testing.T) {
scenario.Provider.githubClient = github.NewClient(nil) scenario.Provider.githubClient = github.NewClient(nil)
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"}, &endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -109,7 +109,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
firstDescription := "description-1" firstDescription := "description-1"
scenarios := []struct { scenarios := []struct {
Name string Name string
Endpoint core.Endpoint Endpoint endpoint.Endpoint
Provider AlertProvider Provider AlertProvider
Alert alert.Alert Alert alert.Alert
NoConditions bool NoConditions bool
@ -117,14 +117,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
}{ }{
{ {
Name: "triggered", Name: "triggered",
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"}, Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`", ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
}, },
{ {
Name: "triggered-with-no-description", Name: "triggered-with-no-description",
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"}, Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{FailureThreshold: 10}, Alert: alert.Alert{FailureThreshold: 10},
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`", ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
@ -132,7 +132,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{ {
Name: "triggered-with-no-conditions", Name: "triggered-with-no-conditions",
NoConditions: true, NoConditions: true,
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"}, Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 10}, Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 10},
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row:\n> description-1", ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row:\n> description-1",
@ -140,9 +140,9 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
var conditionResults []*core.ConditionResult var conditionResults []*endpoint.ConditionResult
if !scenario.NoConditions { if !scenario.NoConditions {
conditionResults = []*core.ConditionResult{ conditionResults = []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: true}, {Condition: "[CONNECTED] == true", Success: true},
{Condition: "[STATUS] == 200", Success: false}, {Condition: "[STATUS] == 200", Success: false},
} }
@ -150,7 +150,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
body := scenario.Provider.buildIssueBody( body := scenario.Provider.buildIssueBody(
&scenario.Endpoint, &scenario.Endpoint,
&scenario.Alert, &scenario.Alert,
&core.Result{ConditionResults: conditionResults}, &endpoint.Result{ConditionResults: conditionResults},
) )
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) { if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body) t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)

View file

@ -11,7 +11,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -51,11 +51,11 @@ func (provider *AlertProvider) IsValid() bool {
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false, // Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
// or closes the relevant issue(s) if the resolved parameter passed is true. // or closes the relevant issue(s) if the resolved parameter passed is true.
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
if len(alert.ResolveKey) == 0 { if len(alert.ResolveKey) == 0 {
alert.ResolveKey = uuid.NewString() alert.ResolveKey = uuid.NewString()
} }
buffer := bytes.NewBuffer(provider.buildAlertBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildAlertBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer) request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
if err != nil { if err != nil {
return err return err
@ -94,21 +94,21 @@ func (provider *AlertProvider) monitoringTool() string {
return "gatus" return "gatus"
} }
func (provider *AlertProvider) service(endpoint *core.Endpoint) string { func (provider *AlertProvider) service(ep *endpoint.Endpoint) string {
if len(provider.Service) > 0 { if len(provider.Service) > 0 {
return provider.Service return provider.Service
} }
return endpoint.DisplayName() return ep.DisplayName()
} }
// buildAlertBody builds the body of the alert // buildAlertBody builds the body of the alert
func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildAlertBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
body := AlertBody{ body := AlertBody{
Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(endpoint)), Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(ep)),
StartTime: result.Timestamp.Format(time.RFC3339), StartTime: result.Timestamp.Format(time.RFC3339),
Service: provider.service(endpoint), Service: provider.service(ep),
MonitoringTool: provider.monitoringTool(), MonitoringTool: provider.monitoringTool(),
Hosts: endpoint.URL, Hosts: ep.URL,
GitlabEnvironmentName: provider.EnvironmentName, GitlabEnvironmentName: provider.EnvironmentName,
Severity: provider.Severity, Severity: provider.Severity,
Fingerprint: alert.ResolveKey, Fingerprint: alert.ResolveKey,
@ -135,9 +135,9 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al
} }
var message string var message string
if resolved { if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
} else { } else {
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
} }
body.Description = message + description + formattedConditionResults body.Description = message + description + formattedConditionResults
bodyAsJSON, _ := json.Marshal(body) bodyAsJSON, _ := json.Marshal(body)

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -84,10 +84,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"}, &endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -108,21 +108,21 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
firstDescription := "description-1" firstDescription := "description-1"
scenarios := []struct { scenarios := []struct {
Name string Name string
Endpoint core.Endpoint Endpoint endpoint.Endpoint
Provider AlertProvider Provider AlertProvider
Alert alert.Alert Alert alert.Alert
ExpectedBody string ExpectedBody string
}{ }{
{ {
Name: "triggered", Name: "triggered",
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"}, Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}", ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
}, },
{ {
Name: "no-description", Name: "no-description",
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"}, Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{FailureThreshold: 10}, Alert: alert.Alert{FailureThreshold: 10},
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 10 time(s) in a row\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}", ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 10 time(s) in a row\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
@ -133,8 +133,8 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
body := scenario.Provider.buildAlertBody( body := scenario.Provider.buildAlertBody(
&scenario.Endpoint, &scenario.Endpoint,
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: true}, {Condition: "[CONNECTED] == true", Success: true},
{Condition: "[STATUS] == 200", Success: false}, {Condition: "[STATUS] == 200", Success: false},
}, },

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the configuration necessary for sending an alert using Google chat // AlertProvider is the configuration necessary for sending an alert using Google chat
@ -50,9 +50,9 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer) request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
if err != nil { if err != nil {
return err return err
} }
@ -112,7 +112,7 @@ type OpenLink struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, color string var message, color string
if resolved { if resolved {
color = "#36A64F" color = "#36A64F"
@ -143,7 +143,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
Widgets: []Widgets{ Widgets: []Widgets{
{ {
KeyValue: &KeyValue{ KeyValue: &KeyValue{
TopLabel: endpoint.DisplayName(), TopLabel: ep.DisplayName(),
Content: message, Content: message,
ContentMultiline: "true", ContentMultiline: "true",
BottomLabel: description, BottomLabel: description,
@ -166,7 +166,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
}, },
}) })
} }
if endpoint.Type() == core.EndpointTypeHTTP { if ep.Type() == endpoint.TypeHTTP {
// We only include a button targeting the URL if the endpoint is an HTTP endpoint // We only include a button targeting the URL if the endpoint is an HTTP endpoint
// If the URL isn't prefixed with https://, Google Chat will just display a blank message aynways. // If the URL isn't prefixed with https://, Google Chat will just display a blank message aynways.
// See https://github.com/TwiN/gatus/issues/362 // See https://github.com/TwiN/gatus/issues/362
@ -175,7 +175,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
{ {
TextButton: TextButton{ TextButton: TextButton{
Text: "URL", Text: "URL",
OnClick: OnClick{OpenLink: OpenLink{URL: endpoint.URL}}, OnClick: OnClick{OpenLink: OpenLink{URL: ep.URL}},
}, },
}, },
}, },

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"}, &endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -141,7 +141,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
secondDescription := "description-2" secondDescription := "description-2"
scenarios := []struct { scenarios := []struct {
Name string Name string
Endpoint core.Endpoint Endpoint endpoint.Endpoint
Provider AlertProvider Provider AlertProvider
Alert alert.Alert Alert alert.Alert
Resolved bool Resolved bool
@ -149,7 +149,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
}{ }{
{ {
Name: "triggered", Name: "triggered",
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"}, Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
@ -157,7 +157,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
}, },
{ {
Name: "resolved", Name: "resolved",
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"}, Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true, Resolved: true,
@ -165,7 +165,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
}, },
{ {
Name: "icmp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362 Name: "icmp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"}, Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
@ -173,7 +173,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
}, },
{ {
Name: "tcp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362 Name: "tcp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"}, Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
@ -185,8 +185,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&scenario.Endpoint, &scenario.Endpoint,
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
const DefaultPriority = 5 const DefaultPriority = 5
@ -41,8 +41,8 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer) request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
if err != nil { if err != nil {
return err return err
@ -67,12 +67,12 @@ type Body struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string var message string
if resolved { if resolved {
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
} else { } else {
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
} }
var formattedConditionResults string var formattedConditionResults string
for _, conditionResult := range result.ConditionResults { for _, conditionResult := range result.ConditionResults {
@ -88,7 +88,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
message += " with the following description: " + alert.GetDescription() message += " with the following description: " + alert.GetDescription()
} }
message += formattedConditionResults message += formattedConditionResults
title := "Gatus: " + endpoint.DisplayName() title := "Gatus: " + ep.DisplayName()
if provider.Title != "" { if provider.Title != "" {
title = provider.Title title = provider.Title
} }

View file

@ -6,7 +6,7 @@ import (
"testing" "testing"
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
func TestAlertProvider_IsValid(t *testing.T) { func TestAlertProvider_IsValid(t *testing.T) {
@ -49,7 +49,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
var ( var (
description = "custom-description" description = "custom-description"
//title = "custom-title" //title = "custom-title"
endpoint = "custom-endpoint" endpointName = "custom-endpoint"
) )
scenarios := []struct { scenarios := []struct {
Name string Name string
@ -63,30 +63,30 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"}, Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description), ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpointName, description),
}, },
{ {
Name: "resolved", Name: "resolved",
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"}, Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true, Resolved: true,
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been resolved after passing successfully 5 time(s) in a row with the following description: %s\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description), ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been resolved after passing successfully 5 time(s) in a row with the following description: %s\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpointName, description),
}, },
{ {
Name: "custom-title", Name: "custom-title",
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"}, Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"custom-title\",\"priority\":0}", endpoint, description), ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"custom-title\",\"priority\":0}", endpointName, description),
}, },
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: endpoint}, &endpoint.Endpoint{Name: endpointName},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the configuration necessary for sending an alert using JetBrains Space // AlertProvider is the configuration necessary for sending an alert using JetBrains Space
@ -46,8 +46,8 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project) url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project)
request, err := http.NewRequest(http.MethodPost, url, buffer) request, err := http.NewRequest(http.MethodPost, url, buffer)
if err != nil { if err != nil {
@ -103,9 +103,9 @@ type Icon struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
body := Body{ body := Body{
Channel: "id:" + provider.getChannelIDForGroup(endpoint.Group), Channel: "id:" + provider.getChannelIDForGroup(ep.Group),
Content: Content{ Content: Content{
ClassName: "ChatMessage.Block", ClassName: "ChatMessage.Block",
Sections: []Section{{ Sections: []Section{{
@ -116,10 +116,10 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
} }
if resolved { if resolved {
body.Content.Style = "SUCCESS" body.Content.Style = "SUCCESS"
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
} else { } else {
body.Content.Style = "WARNING" body.Content.Style = "WARNING"
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
} }
for _, conditionResult := range result.ConditionResults { for _, conditionResult := range result.ConditionResults {
icon := "warning" icon := "warning"

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -120,10 +120,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -146,7 +146,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
scenarios := []struct { scenarios := []struct {
Name string Name string
Provider AlertProvider Provider AlertProvider
Endpoint core.Endpoint Endpoint endpoint.Endpoint
Alert alert.Alert Alert alert.Alert
Resolved bool Resolved bool
ExpectedBody string ExpectedBody string
@ -154,7 +154,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{ {
Name: "triggered", Name: "triggered",
Provider: AlertProvider{}, Provider: AlertProvider{},
Endpoint: core.Endpoint{Name: "name"}, Endpoint: endpoint.Endpoint{Name: "name"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been triggered due to having failed 3 time(s) in a row"}]}}`, ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
@ -162,7 +162,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{ {
Name: "triggered-with-group", Name: "triggered-with-group",
Provider: AlertProvider{}, Provider: AlertProvider{},
Endpoint: core.Endpoint{Name: "name", Group: "group"}, Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row"}]}}`, ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
@ -170,7 +170,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{ {
Name: "resolved", Name: "resolved",
Provider: AlertProvider{}, Provider: AlertProvider{},
Endpoint: core.Endpoint{Name: "name"}, Endpoint: endpoint.Endpoint{Name: "name"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true, Resolved: true,
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been resolved after passing successfully 5 time(s) in a row"}]}}`, ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
@ -178,7 +178,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{ {
Name: "resolved-with-group", Name: "resolved-with-group",
Provider: AlertProvider{}, Provider: AlertProvider{},
Endpoint: core.Endpoint{Name: "name", Group: "group"}, Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true, Resolved: true,
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row"}]}}`, ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
@ -189,8 +189,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&scenario.Endpoint, &scenario.Endpoint,
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -12,7 +12,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the configuration necessary for sending an alert using Matrix // AlertProvider is the configuration necessary for sending an alert using Matrix
@ -61,9 +61,9 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
config := provider.getConfigForGroup(endpoint.Group) config := provider.getConfigForGroup(ep.Group)
if config.ServerURL == "" { if config.ServerURL == "" {
config.ServerURL = defaultServerURL config.ServerURL = defaultServerURL
} }
@ -103,23 +103,23 @@ type Body struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
body, _ := json.Marshal(Body{ body, _ := json.Marshal(Body{
MsgType: "m.text", MsgType: "m.text",
Format: "org.matrix.custom.html", Format: "org.matrix.custom.html",
Body: buildPlaintextMessageBody(endpoint, alert, result, resolved), Body: buildPlaintextMessageBody(ep, alert, result, resolved),
FormattedBody: buildHTMLMessageBody(endpoint, alert, result, resolved), FormattedBody: buildHTMLMessageBody(ep, alert, result, resolved),
}) })
return body return body
} }
// buildPlaintextMessageBody builds the message body in plaintext to include in request // buildPlaintextMessageBody builds the message body in plaintext to include in request
func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string { func buildPlaintextMessageBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
var message string var message string
if resolved { if resolved {
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
} else { } else {
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
} }
var formattedConditionResults string var formattedConditionResults string
for _, conditionResult := range result.ConditionResults { for _, conditionResult := range result.ConditionResults {
@ -139,12 +139,12 @@ func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, resu
} }
// buildHTMLMessageBody builds the message body in HTML to include in request // buildHTMLMessageBody builds the message body in HTML to include in request
func buildHTMLMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string { func buildHTMLMessageBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
var message string var message string
if resolved { if resolved {
message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
} else { } else {
message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
} }
var formattedConditionResults string var formattedConditionResults string
if len(result.ConditionResults) > 0 { if len(result.ConditionResults) > 0 {

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -149,10 +149,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -197,10 +197,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the configuration necessary for sending an alert using Mattermost // AlertProvider is the configuration necessary for sending an alert using Mattermost
@ -50,9 +50,9 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved))) buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer) request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
if err != nil { if err != nil {
return err return err
} }
@ -92,13 +92,13 @@ type Field struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, color string var message, color string
if resolved { if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
color = "#36A64F" color = "#36A64F"
} else { } else {
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
color = "#DD0000" color = "#DD0000"
} }
var formattedConditionResults string var formattedConditionResults string

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -120,10 +120,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -168,10 +168,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
const ( const (
@ -33,8 +33,8 @@ func (provider *AlertProvider) IsValid() bool {
// Send an alert using the provider // Send an alert using the provider
// Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms // Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer) request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
if err != nil { if err != nil {
return err return err
@ -60,12 +60,12 @@ type Body struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string var message string
if resolved { if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
} else { } else {
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
} }
body, _ := json.Marshal(Body{ body, _ := json.Marshal(Body{
Originator: provider.Originator, Originator: provider.Originator,

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -83,10 +83,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -131,10 +131,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -11,7 +11,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
const ( const (
@ -46,8 +46,8 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer) request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
if err != nil { if err != nil {
return err return err
@ -77,7 +77,7 @@ type Body struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, formattedConditionResults, tag string var message, formattedConditionResults, tag string
if resolved { if resolved {
tag = "white_check_mark" tag = "white_check_mark"
@ -101,7 +101,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
message += formattedConditionResults message += formattedConditionResults
body, _ := json.Marshal(Body{ body, _ := json.Marshal(Body{
Topic: provider.Topic, Topic: provider.Topic,
Title: "Gatus: " + endpoint.DisplayName(), Title: "Gatus: " + ep.DisplayName(),
Message: message, Message: message,
Tags: []string{tag}, Tags: []string{tag},
Priority: provider.Priority, Priority: provider.Priority,

View file

@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
func TestAlertDefaultProvider_IsValid(t *testing.T) { func TestAlertDefaultProvider_IsValid(t *testing.T) {
@ -92,10 +92,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -11,7 +11,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
const ( const (
@ -59,13 +59,13 @@ func (provider *AlertProvider) IsValid() bool {
// Send an alert using the provider // Send an alert using the provider
// //
// Relevant: https://docs.opsgenie.com/docs/alert-api // Relevant: https://docs.opsgenie.com/docs/alert-api
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
err := provider.createAlert(endpoint, alert, result, resolved) err := provider.createAlert(ep, alert, result, resolved)
if err != nil { if err != nil {
return err return err
} }
if resolved { if resolved {
err = provider.closeAlert(endpoint, alert) err = provider.closeAlert(ep, alert)
if err != nil { if err != nil {
return err return err
} }
@ -75,20 +75,20 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey // The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
alert.ResolveKey = "" alert.ResolveKey = ""
} else { } else {
alert.ResolveKey = provider.alias(buildKey(endpoint)) alert.ResolveKey = provider.alias(buildKey(ep))
} }
} }
return nil return nil
} }
func (provider *AlertProvider) createAlert(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) createAlert(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
payload := provider.buildCreateRequestBody(endpoint, alert, result, resolved) payload := provider.buildCreateRequestBody(ep, alert, result, resolved)
return provider.sendRequest(restAPI, http.MethodPost, payload) return provider.sendRequest(restAPI, http.MethodPost, payload)
} }
func (provider *AlertProvider) closeAlert(endpoint *core.Endpoint, alert *alert.Alert) error { func (provider *AlertProvider) closeAlert(ep *endpoint.Endpoint, alert *alert.Alert) error {
payload := provider.buildCloseRequestBody(endpoint, alert) payload := provider.buildCloseRequestBody(ep, alert)
url := restAPI + "/" + provider.alias(buildKey(endpoint)) + "/close?identifierType=alias" url := restAPI + "/" + provider.alias(buildKey(ep)) + "/close?identifierType=alias"
return provider.sendRequest(url, http.MethodPost, payload) return provider.sendRequest(url, http.MethodPost, payload)
} }
@ -115,17 +115,17 @@ func (provider *AlertProvider) sendRequest(url, method string, payload interface
return nil return nil
} }
func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) alertCreateRequest { func (provider *AlertProvider) buildCreateRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) alertCreateRequest {
var message, description string var message, description string
if resolved { if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()) message = fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription())
description = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) description = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
} else { } else {
message = fmt.Sprintf("%s - %s", endpoint.Name, alert.GetDescription()) message = fmt.Sprintf("%s - %s", ep.Name, alert.GetDescription())
description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
} }
if endpoint.Group != "" { if ep.Group != "" {
message = fmt.Sprintf("[%s] %s", endpoint.Group, message) message = fmt.Sprintf("[%s] %s", ep.Group, message)
} }
var formattedConditionResults string var formattedConditionResults string
for _, conditionResult := range result.ConditionResults { for _, conditionResult := range result.ConditionResults {
@ -138,10 +138,10 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
} }
description = description + "\n" + formattedConditionResults description = description + "\n" + formattedConditionResults
key := buildKey(endpoint) key := buildKey(ep)
details := map[string]string{ details := map[string]string{
"endpoint:url": endpoint.URL, "endpoint:url": ep.URL,
"endpoint:group": endpoint.Group, "endpoint:group": ep.Group,
"result:hostname": result.Hostname, "result:hostname": result.Hostname,
"result:ip": result.IP, "result:ip": result.IP,
"result:dns_code": result.DNSRCode, "result:dns_code": result.DNSRCode,
@ -167,10 +167,10 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
} }
} }
func (provider *AlertProvider) buildCloseRequestBody(endpoint *core.Endpoint, alert *alert.Alert) alertCloseRequest { func (provider *AlertProvider) buildCloseRequestBody(ep *endpoint.Endpoint, alert *alert.Alert) alertCloseRequest {
return alertCloseRequest{ return alertCloseRequest{
Source: buildKey(endpoint), Source: buildKey(ep),
Note: fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()), Note: fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription()),
} }
} }
@ -211,12 +211,12 @@ func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert return provider.DefaultAlert
} }
func buildKey(endpoint *core.Endpoint) string { func buildKey(ep *endpoint.Endpoint) string {
name := toKebabCase(endpoint.Name) name := toKebabCase(ep.Name)
if endpoint.Group == "" { if ep.Group == "" {
return name return name
} }
return toKebabCase(endpoint.Group) + "-" + name return toKebabCase(ep.Group) + "-" + name
} }
func toKebabCase(val string) string { func toKebabCase(val string) string {

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -79,10 +79,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -106,8 +106,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
Name string Name string
Provider *AlertProvider Provider *AlertProvider
Alert *alert.Alert Alert *alert.Alert
Endpoint *core.Endpoint Endpoint *endpoint.Endpoint
Result *core.Result Result *endpoint.Result
Resolved bool Resolved bool
want alertCreateRequest want alertCreateRequest
}{ }{
@ -115,8 +115,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
Name: "missing all params (unresolved)", Name: "missing all params (unresolved)",
Provider: &AlertProvider{}, Provider: &AlertProvider{},
Alert: &alert.Alert{}, Alert: &alert.Alert{},
Endpoint: &core.Endpoint{}, Endpoint: &endpoint.Endpoint{},
Result: &core.Result{}, Result: &endpoint.Result{},
Resolved: false, Resolved: false,
want: alertCreateRequest{ want: alertCreateRequest{
Message: " - ", Message: " - ",
@ -133,8 +133,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
Name: "missing all params (resolved)", Name: "missing all params (resolved)",
Provider: &AlertProvider{}, Provider: &AlertProvider{},
Alert: &alert.Alert{}, Alert: &alert.Alert{},
Endpoint: &core.Endpoint{}, Endpoint: &endpoint.Endpoint{},
Result: &core.Result{}, Result: &endpoint.Result{},
Resolved: true, Resolved: true,
want: alertCreateRequest{ want: alertCreateRequest{
Message: "RESOLVED: - ", Message: "RESOLVED: - ",
@ -154,11 +154,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
Description: &description, Description: &description,
FailureThreshold: 3, FailureThreshold: 3,
}, },
Endpoint: &core.Endpoint{ Endpoint: &endpoint.Endpoint{
Name: "my super app", Name: "my super app",
}, },
Result: &core.Result{ Result: &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: true, Success: true,
@ -194,11 +194,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
Description: &description, Description: &description,
SuccessThreshold: 4, SuccessThreshold: 4,
}, },
Endpoint: &core.Endpoint{ Endpoint: &endpoint.Endpoint{
Name: "my mega app", Name: "my mega app",
}, },
Result: &core.Result{ Result: &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: true, Success: true,
@ -226,17 +226,17 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
Description: &description, Description: &description,
FailureThreshold: 6, FailureThreshold: 6,
}, },
Endpoint: &core.Endpoint{ Endpoint: &endpoint.Endpoint{
Name: "my app", Name: "my app",
Group: "end game", Group: "end game",
URL: "https://my.go/app", URL: "https://my.go/app",
}, },
Result: &core.Result{ Result: &endpoint.Result{
HTTPStatus: 400, HTTPStatus: 400,
Hostname: "my.go", Hostname: "my.go",
Errors: []string{"error 01", "error 02"}, Errors: []string{"error 01", "error 02"},
Success: false, Success: false,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: false, Success: false,
@ -279,14 +279,14 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
Name string Name string
Provider *AlertProvider Provider *AlertProvider
Alert *alert.Alert Alert *alert.Alert
Endpoint *core.Endpoint Endpoint *endpoint.Endpoint
want alertCloseRequest want alertCloseRequest
}{ }{
{ {
Name: "Missing all values", Name: "Missing all values",
Provider: &AlertProvider{}, Provider: &AlertProvider{},
Alert: &alert.Alert{}, Alert: &alert.Alert{},
Endpoint: &core.Endpoint{}, Endpoint: &endpoint.Endpoint{},
want: alertCloseRequest{ want: alertCloseRequest{
Source: "", Source: "",
Note: "RESOLVED: - ", Note: "RESOLVED: - ",
@ -298,7 +298,7 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
Alert: &alert.Alert{ Alert: &alert.Alert{
Description: &description, Description: &description,
}, },
Endpoint: &core.Endpoint{ Endpoint: &endpoint.Endpoint{
Name: "endpoint name", Name: "endpoint name",
}, },
want: alertCloseRequest{ want: alertCloseRequest{

View file

@ -10,7 +10,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
const ( const (
@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
// Send an alert using the provider // Send an alert using the provider
// //
// Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/ // Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer) request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
if err != nil { if err != nil {
return err return err
@ -101,19 +101,19 @@ type Payload struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, eventAction, resolveKey string var message, eventAction, resolveKey string
if resolved { if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
eventAction = "resolve" eventAction = "resolve"
resolveKey = alert.ResolveKey resolveKey = alert.ResolveKey
} else { } else {
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
eventAction = "trigger" eventAction = "trigger"
resolveKey = "" resolveKey = ""
} }
body, _ := json.Marshal(Body{ body, _ := json.Marshal(Body{
RoutingKey: provider.getIntegrationKeyForGroup(endpoint.Group), RoutingKey: provider.getIntegrationKeyForGroup(ep.Group),
DedupKey: resolveKey, DedupKey: resolveKey,
EventAction: eventAction, EventAction: eventAction,
Payload: Payload{ Payload: Payload{

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -115,10 +115,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -161,7 +161,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody(&core.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &core.Result{}, scenario.Resolved) body := scenario.Provider.buildRequestBody(&endpoint.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &endpoint.Result{}, scenario.Resolved)
if string(body) != scenario.ExpectedBody { if string(body) != scenario.ExpectedBody {
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body) t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
} }

View file

@ -21,7 +21,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/teams" "github.com/TwiN/gatus/v5/alerting/provider/teams"
"github.com/TwiN/gatus/v5/alerting/provider/telegram" "github.com/TwiN/gatus/v5/alerting/provider/telegram"
"github.com/TwiN/gatus/v5/alerting/provider/twilio" "github.com/TwiN/gatus/v5/alerting/provider/twilio"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the interface that each provider should implement // AlertProvider is the interface that each provider should implement
@ -33,7 +33,7 @@ type AlertProvider interface {
GetDefaultAlert() *alert.Alert GetDefaultAlert() *alert.Alert
// Send an alert using the provider // Send an alert using the provider
Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error
} }
// ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline // ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
const ( const (
@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
// Send an alert using the provider // Send an alert using the provider
// Reference doc for pushover: https://pushover.net/api // Reference doc for pushover: https://pushover.net/api
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer) request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
if err != nil { if err != nil {
return err return err
@ -81,12 +81,12 @@ type Body struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string var message string
if resolved { if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
} else { } else {
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
} }
body, _ := json.Marshal(Body{ body, _ := json.Marshal(Body{
Token: provider.ApplicationToken, Token: provider.ApplicationToken,

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -95,10 +95,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -150,10 +150,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the configuration necessary for sending an alert using Slack // AlertProvider is the configuration necessary for sending an alert using Slack
@ -42,9 +42,9 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer) request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
if err != nil { if err != nil {
return err return err
} }
@ -81,13 +81,13 @@ type Field struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, color string var message, color string
if resolved { if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
color = "#36A64F" color = "#36A64F"
} else { } else {
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
color = "#DD0000" color = "#DD0000"
} }
var formattedConditionResults string var formattedConditionResults string

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -142,7 +142,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
scenarios := []struct { scenarios := []struct {
Name string Name string
Provider AlertProvider Provider AlertProvider
Endpoint core.Endpoint Endpoint endpoint.Endpoint
Alert alert.Alert Alert alert.Alert
NoConditions bool NoConditions bool
Resolved bool Resolved bool
@ -151,7 +151,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{ {
Name: "triggered", Name: "triggered",
Provider: AlertProvider{}, Provider: AlertProvider{},
Endpoint: core.Endpoint{Name: "name"}, Endpoint: endpoint.Endpoint{Name: "name"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}", ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
@ -159,7 +159,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{ {
Name: "triggered-with-group", Name: "triggered-with-group",
Provider: AlertProvider{}, Provider: AlertProvider{},
Endpoint: core.Endpoint{Name: "name", Group: "group"}, Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}", ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
@ -168,7 +168,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Name: "triggered-with-no-conditions", Name: "triggered-with-no-conditions",
NoConditions: true, NoConditions: true,
Provider: AlertProvider{}, Provider: AlertProvider{},
Endpoint: core.Endpoint{Name: "name"}, Endpoint: endpoint.Endpoint{Name: "name"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\"}]}", ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\"}]}",
@ -176,7 +176,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{ {
Name: "resolved", Name: "resolved",
Provider: AlertProvider{}, Provider: AlertProvider{},
Endpoint: core.Endpoint{Name: "name"}, Endpoint: endpoint.Endpoint{Name: "name"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true, Resolved: true,
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}", ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
@ -184,7 +184,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{ {
Name: "resolved-with-group", Name: "resolved-with-group",
Provider: AlertProvider{}, Provider: AlertProvider{},
Endpoint: core.Endpoint{Name: "name", Group: "group"}, Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true, Resolved: true,
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}", ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
@ -192,9 +192,9 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
var conditionResults []*core.ConditionResult var conditionResults []*endpoint.ConditionResult
if !scenario.NoConditions { if !scenario.NoConditions {
conditionResults = []*core.ConditionResult{ conditionResults = []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
} }
@ -202,7 +202,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&scenario.Endpoint, &scenario.Endpoint,
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: conditionResults, ConditionResults: conditionResults,
}, },
scenario.Resolved, scenario.Resolved,

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the configuration necessary for sending an alert using Teams // AlertProvider is the configuration necessary for sending an alert using Teams
@ -21,7 +21,7 @@ type AlertProvider struct {
// Overrides is a list of Override that may be prioritized over the default configuration // Overrides is a list of Override that may be prioritized over the default configuration
Overrides []Override `yaml:"overrides,omitempty"` Overrides []Override `yaml:"overrides,omitempty"`
// Title is the title of the message that will be sent // Title is the title of the message that will be sent
Title string `yaml:"title,omitempty"` Title string `yaml:"title,omitempty"`
} }
@ -47,9 +47,9 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer) request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
if err != nil { if err != nil {
return err return err
} }
@ -81,13 +81,13 @@ type Section struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, color string var message, color string
if resolved { if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
color = "#36A64F" color = "#36A64F"
} else { } else {
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
color = "#DD0000" color = "#DD0000"
} }
var formattedConditionResults string var formattedConditionResults string

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -172,17 +172,17 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
var conditionResults []*core.ConditionResult var conditionResults []*endpoint.ConditionResult
if !scenario.NoConditions { if !scenario.NoConditions {
conditionResults = []*core.ConditionResult{ conditionResults = []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
} }
} }
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ConditionResults: conditionResults}, &endpoint.Result{ConditionResults: conditionResults},
scenario.Resolved, scenario.Resolved,
) )
if string(body) != scenario.ExpectedBody { if string(body) != scenario.ExpectedBody {

View file

@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
const defaultAPIURL = "https://api.telegram.org" const defaultAPIURL = "https://api.telegram.org"
@ -36,8 +36,8 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
apiURL := provider.APIURL apiURL := provider.APIURL
if apiURL == "" { if apiURL == "" {
apiURL = defaultAPIURL apiURL = defaultAPIURL
@ -66,12 +66,12 @@ type Body struct {
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string var message string
if resolved { if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", ep.DisplayName(), alert.SuccessThreshold)
} else { } else {
message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", ep.DisplayName(), alert.FailureThreshold)
} }
var formattedConditionResults string var formattedConditionResults string
if len(result.ConditionResults) > 0 { if len(result.ConditionResults) > 0 {

View file

@ -7,7 +7,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -89,10 +89,10 @@ func TestAlertProvider_Send(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send( err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },
@ -145,17 +145,17 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
var conditionResults []*core.ConditionResult var conditionResults []*endpoint.ConditionResult
if !scenario.NoConditions { if !scenario.NoConditions {
conditionResults = []*core.ConditionResult{ conditionResults = []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
} }
} }
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ConditionResults: conditionResults}, &endpoint.Result{ConditionResults: conditionResults},
scenario.Resolved, scenario.Resolved,
) )
if string(body) != scenario.ExpectedBody { if string(body) != scenario.ExpectedBody {

View file

@ -10,7 +10,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
// AlertProvider is the configuration necessary for sending an alert using Twilio // AlertProvider is the configuration necessary for sending an alert using Twilio
@ -30,8 +30,8 @@ func (provider *AlertProvider) IsValid() bool {
} }
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved))) buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer) request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer)
if err != nil { if err != nil {
return err return err
@ -51,12 +51,12 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
} }
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string { func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
var message string var message string
if resolved { if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
} else { } else {
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
} }
return url.Values{ return url.Values{
"To": {provider.To}, "To": {provider.To},

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
func TestTwilioAlertProvider_IsValid(t *testing.T) { func TestTwilioAlertProvider_IsValid(t *testing.T) {
@ -51,10 +51,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &endpoint.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved},
}, },

View file

@ -9,7 +9,7 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/core/ui" "github.com/TwiN/gatus/v5/config/endpoint/ui"
"github.com/TwiN/gatus/v5/storage/store" "github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"

View file

@ -8,8 +8,8 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/core/ui" "github.com/TwiN/gatus/v5/config/endpoint/ui"
"github.com/TwiN/gatus/v5/storage/store" "github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/watchdog" "github.com/TwiN/gatus/v5/watchdog"
) )
@ -19,7 +19,7 @@ func TestBadge(t *testing.T) {
defer cache.Clear() defer cache.Clear()
cfg := &config.Config{ cfg := &config.Config{
Metrics: true, Metrics: true,
Endpoints: []*core.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
Name: "frontend", Name: "frontend",
Group: "core", Group: "core",
@ -34,8 +34,8 @@ func TestBadge(t *testing.T) {
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig() cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig() cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()}) watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()}) watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
api := New(cfg) api := New(cfg)
router := api.Router() router := api.Router()
type Scenario struct { type Scenario struct {
@ -218,30 +218,30 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
defer cache.Clear() defer cache.Clear()
var ( var (
firstCondition = core.Condition("[STATUS] == 200") firstCondition = endpoint.Condition("[STATUS] == 200")
secondCondition = core.Condition("[RESPONSE_TIME] < 500") secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h") thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
) )
firstTestEndpoint := core.Endpoint{ firstTestEndpoint := endpoint.Endpoint{
Name: "a", Name: "a",
URL: "https://example.org/what/ever", URL: "https://example.org/what/ever",
Method: "GET", Method: "GET",
Body: "body", Body: "body",
Interval: 30 * time.Second, Interval: 30 * time.Second,
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition}, Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
Alerts: nil, Alerts: nil,
NumberOfFailuresInARow: 0, NumberOfFailuresInARow: 0,
NumberOfSuccessesInARow: 0, NumberOfSuccessesInARow: 0,
UIConfig: ui.GetDefaultConfig(), UIConfig: ui.GetDefaultConfig(),
} }
secondTestEndpoint := core.Endpoint{ secondTestEndpoint := endpoint.Endpoint{
Name: "b", Name: "b",
URL: "https://example.org/what/ever", URL: "https://example.org/what/ever",
Method: "GET", Method: "GET",
Body: "body", Body: "body",
Interval: 30 * time.Second, Interval: 30 * time.Second,
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition}, Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
Alerts: nil, Alerts: nil,
NumberOfFailuresInARow: 0, NumberOfFailuresInARow: 0,
NumberOfSuccessesInARow: 0, NumberOfSuccessesInARow: 0,
@ -255,10 +255,10 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
} }
cfg := &config.Config{ cfg := &config.Config{
Metrics: true, Metrics: true,
Endpoints: []*core.Endpoint{&firstTestEndpoint, &secondTestEndpoint}, Endpoints: []*endpoint.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
} }
testSuccessfulResult := core.Result{ testSuccessfulResult := endpoint.Result{
Hostname: "example.org", Hostname: "example.org",
IP: "127.0.0.1", IP: "127.0.0.1",
HTTPStatus: 200, HTTPStatus: 200,
@ -268,7 +268,7 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
Timestamp: time.Now(), Timestamp: time.Now(),
Duration: 150 * time.Millisecond, Duration: 150 * time.Millisecond,
CertificateExpiration: 10 * time.Hour, CertificateExpiration: 10 * time.Hour,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: true, Success: true,

View file

@ -7,7 +7,7 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store" "github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/watchdog" "github.com/TwiN/gatus/v5/watchdog"
) )
@ -17,7 +17,7 @@ func TestResponseTimeChart(t *testing.T) {
defer cache.Clear() defer cache.Clear()
cfg := &config.Config{ cfg := &config.Config{
Metrics: true, Metrics: true,
Endpoints: []*core.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
Name: "frontend", Name: "frontend",
Group: "core", Group: "core",
@ -28,8 +28,8 @@ func TestResponseTimeChart(t *testing.T) {
}, },
}, },
} }
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
api := New(cfg) api := New(cfg)
router := api.Router() router := api.Router()
type Scenario struct { type Scenario struct {

View file

@ -9,8 +9,8 @@ import (
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/config/remote" "github.com/TwiN/gatus/v5/config/remote"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/storage/store" "github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
@ -51,11 +51,11 @@ func EndpointStatuses(cfg *config.Config) fiber.Handler {
} }
} }
func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*core.EndpointStatus, error) { func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*endpoint.Status, error) {
if remoteConfig == nil || len(remoteConfig.Instances) == 0 { if remoteConfig == nil || len(remoteConfig.Instances) == 0 {
return nil, nil return nil, nil
} }
var endpointStatusesFromAllRemotes []*core.EndpointStatus var endpointStatusesFromAllRemotes []*endpoint.Status
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig) httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
for _, instance := range remoteConfig.Instances { for _, instance := range remoteConfig.Instances {
response, err := httpClient.Get(instance.URL) response, err := httpClient.Get(instance.URL)
@ -68,7 +68,7 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*cor
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error()) log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
continue continue
} }
var endpointStatuses []*core.EndpointStatus var endpointStatuses []*endpoint.Status
if err = json.Unmarshal(body, &endpointStatuses); err != nil { if err = json.Unmarshal(body, &endpointStatuses); err != nil {
_ = response.Body.Close() _ = response.Body.Close()
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error()) log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
@ -83,7 +83,7 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*cor
return endpointStatusesFromAllRemotes, nil return endpointStatusesFromAllRemotes, nil
} }
// EndpointStatus retrieves a single core.EndpointStatus by group and endpoint name // EndpointStatus retrieves a single endpoint.Status by group and endpoint name
func EndpointStatus(c *fiber.Ctx) error { func EndpointStatus(c *fiber.Ctx) error {
page, pageSize := extractPageAndPageSizeFromRequest(c) page, pageSize := extractPageAndPageSizeFromRequest(c)
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents)) endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))

View file

@ -8,7 +8,7 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store" "github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/watchdog" "github.com/TwiN/gatus/v5/watchdog"
) )
@ -16,19 +16,19 @@ import (
var ( var (
timestamp = time.Now() timestamp = time.Now()
testEndpoint = core.Endpoint{ testEndpoint = endpoint.Endpoint{
Name: "name", Name: "name",
Group: "group", Group: "group",
URL: "https://example.org/what/ever", URL: "https://example.org/what/ever",
Method: "GET", Method: "GET",
Body: "body", Body: "body",
Interval: 30 * time.Second, Interval: 30 * time.Second,
Conditions: []core.Condition{core.Condition("[STATUS] == 200"), core.Condition("[RESPONSE_TIME] < 500"), core.Condition("[CERTIFICATE_EXPIRATION] < 72h")}, Conditions: []endpoint.Condition{endpoint.Condition("[STATUS] == 200"), endpoint.Condition("[RESPONSE_TIME] < 500"), endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")},
Alerts: nil, Alerts: nil,
NumberOfFailuresInARow: 0, NumberOfFailuresInARow: 0,
NumberOfSuccessesInARow: 0, NumberOfSuccessesInARow: 0,
} }
testSuccessfulResult = core.Result{ testSuccessfulResult = endpoint.Result{
Hostname: "example.org", Hostname: "example.org",
IP: "127.0.0.1", IP: "127.0.0.1",
HTTPStatus: 200, HTTPStatus: 200,
@ -38,7 +38,7 @@ var (
Timestamp: timestamp, Timestamp: timestamp,
Duration: 150 * time.Millisecond, Duration: 150 * time.Millisecond,
CertificateExpiration: 10 * time.Hour, CertificateExpiration: 10 * time.Hour,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: true, Success: true,
@ -53,7 +53,7 @@ var (
}, },
}, },
} }
testUnsuccessfulResult = core.Result{ testUnsuccessfulResult = endpoint.Result{
Hostname: "example.org", Hostname: "example.org",
IP: "127.0.0.1", IP: "127.0.0.1",
HTTPStatus: 200, HTTPStatus: 200,
@ -63,7 +63,7 @@ var (
Timestamp: timestamp, Timestamp: timestamp,
Duration: 750 * time.Millisecond, Duration: 750 * time.Millisecond,
CertificateExpiration: 10 * time.Hour, CertificateExpiration: 10 * time.Hour,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: true, Success: true,
@ -85,7 +85,7 @@ func TestEndpointStatus(t *testing.T) {
defer cache.Clear() defer cache.Clear()
cfg := &config.Config{ cfg := &config.Config{
Metrics: true, Metrics: true,
Endpoints: []*core.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
Name: "frontend", Name: "frontend",
Group: "core", Group: "core",
@ -96,8 +96,8 @@ func TestEndpointStatus(t *testing.T) {
}, },
}, },
} }
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
api := New(cfg) api := New(cfg)
router := api.Router() router := api.Router()
type Scenario struct { type Scenario struct {

View file

@ -7,7 +7,7 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store" "github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/watchdog" "github.com/TwiN/gatus/v5/watchdog"
@ -41,7 +41,7 @@ func CreateExternalEndpointResult(cfg *config.Config) fiber.Handler {
return c.Status(401).SendString("invalid token") return c.Status(401).SendString("invalid token")
} }
// Persist the result in the storage // Persist the result in the storage
result := &core.Result{ result := &endpoint.Result{
Timestamp: time.Now(), Timestamp: time.Now(),
Success: c.QueryBool("success"), Success: c.QueryBool("success"),
Errors: []string{}, Errors: []string{},

View file

@ -9,8 +9,8 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/alerting/provider/discord" "github.com/TwiN/gatus/v5/alerting/provider/discord"
"github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/config/maintenance" "github.com/TwiN/gatus/v5/config/maintenance"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/storage/store" "github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
) )
@ -22,7 +22,7 @@ func TestCreateExternalEndpointResult(t *testing.T) {
Alerting: &alerting.Config{ Alerting: &alerting.Config{
Discord: &discord.AlertProvider{}, Discord: &discord.AlertProvider{},
}, },
ExternalEndpoints: []*core.ExternalEndpoint{ ExternalEndpoints: []*endpoint.ExternalEndpoint{
{ {
Name: "n", Name: "n",
Group: "g", Group: "g",

View file

@ -9,8 +9,8 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/config/ui" "github.com/TwiN/gatus/v5/config/ui"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/storage/store" "github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/watchdog" "github.com/TwiN/gatus/v5/watchdog"
) )
@ -20,7 +20,7 @@ func TestSinglePageApplication(t *testing.T) {
defer cache.Clear() defer cache.Clear()
cfg := &config.Config{ cfg := &config.Config{
Metrics: true, Metrics: true,
Endpoints: []*core.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
Name: "frontend", Name: "frontend",
Group: "core", Group: "core",
@ -34,8 +34,8 @@ func TestSinglePageApplication(t *testing.T) {
Title: "example-title", Title: "example-title",
}, },
} }
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
api := New(cfg) api := New(cfg)
router := api.Router() router := api.Router()
type Scenario struct { type Scenario struct {

View file

@ -16,11 +16,16 @@ import (
"github.com/TwiN/gocache/v2" "github.com/TwiN/gocache/v2"
"github.com/TwiN/whois" "github.com/TwiN/whois"
"github.com/ishidawataru/sctp" "github.com/ishidawataru/sctp"
"github.com/miekg/dns"
ping "github.com/prometheus-community/pro-bing" ping "github.com/prometheus-community/pro-bing"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
) )
const (
dnsPort = 53
)
var ( var (
// injectedHTTPClient is used for testing purposes // injectedHTTPClient is used for testing purposes
injectedHTTPClient *http.Client injectedHTTPClient *http.Client
@ -291,6 +296,49 @@ func QueryWebSocket(address, body string, config *Config) (bool, []byte, error)
return true, msg[:n], nil return true, msg[:n], nil
} }
func QueryDNS(queryType, queryName, url string) (connected bool, dnsRcode string, body []byte, err error) {
if !strings.Contains(url, ":") {
url = fmt.Sprintf("%s:%d", url, dnsPort)
}
queryTypeAsUint16 := dns.StringToType[queryType]
c := new(dns.Client)
m := new(dns.Msg)
m.SetQuestion(queryName, queryTypeAsUint16)
r, _, err := c.Exchange(m, url)
if err != nil {
return false, "", nil, err
}
connected = true
dnsRcode = dns.RcodeToString[r.Rcode]
for _, rr := range r.Answer {
switch rr.Header().Rrtype {
case dns.TypeA:
if a, ok := rr.(*dns.A); ok {
body = []byte(a.A.String())
}
case dns.TypeAAAA:
if aaaa, ok := rr.(*dns.AAAA); ok {
body = []byte(aaaa.AAAA.String())
}
case dns.TypeCNAME:
if cname, ok := rr.(*dns.CNAME); ok {
body = []byte(cname.Target)
}
case dns.TypeMX:
if mx, ok := rr.(*dns.MX); ok {
body = []byte(mx.Mx)
}
case dns.TypeNS:
if ns, ok := rr.(*dns.NS); ok {
body = []byte(ns.Ns)
}
default:
body = []byte("query type is not supported yet")
}
}
return connected, dnsRcode, body, nil
}
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes // InjectHTTPClient is used to inject a custom HTTP client for testing purposes
func InjectHTTPClient(httpClient *http.Client) { func InjectHTTPClient(httpClient *http.Client) {
injectedHTTPClient = httpClient injectedHTTPClient = httpClient

View file

@ -8,6 +8,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/TwiN/gatus/v5/config/endpoint/dns"
"github.com/TwiN/gatus/v5/pattern"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -334,3 +336,97 @@ func TestTlsRenegotiation(t *testing.T) {
}) })
} }
} }
func TestQueryDNS(t *testing.T) {
tests := []struct {
name string
inputDNS dns.Config
inputURL string
expectedDNSCode string
expectedBody string
isErrExpected bool
}{
{
name: "test Config with type A",
inputDNS: dns.Config{
QueryType: "A",
QueryName: "example.com.",
},
inputURL: "8.8.8.8",
expectedDNSCode: "NOERROR",
expectedBody: "93.184.215.14",
},
{
name: "test Config with type AAAA",
inputDNS: dns.Config{
QueryType: "AAAA",
QueryName: "example.com.",
},
inputURL: "8.8.8.8",
expectedDNSCode: "NOERROR",
expectedBody: "2606:2800:21f:cb07:6820:80da:af6b:8b2c",
},
{
name: "test Config with type CNAME",
inputDNS: dns.Config{
QueryType: "CNAME",
QueryName: "en.wikipedia.org.",
},
inputURL: "8.8.8.8",
expectedDNSCode: "NOERROR",
expectedBody: "dyna.wikimedia.org.",
},
{
name: "test Config with type MX",
inputDNS: dns.Config{
QueryType: "MX",
QueryName: "example.com.",
},
inputURL: "8.8.8.8",
expectedDNSCode: "NOERROR",
expectedBody: ".",
},
{
name: "test Config with type NS",
inputDNS: dns.Config{
QueryType: "NS",
QueryName: "example.com.",
},
inputURL: "8.8.8.8",
expectedDNSCode: "NOERROR",
expectedBody: "*.iana-servers.net.",
},
{
name: "test Config with fake type and retrieve error",
inputDNS: dns.Config{
QueryType: "B",
QueryName: "example",
},
inputURL: "8.8.8.8",
isErrExpected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, dnsRCode, body, err := QueryDNS(test.inputDNS.QueryType, test.inputDNS.QueryName, test.inputURL)
if test.isErrExpected && err == nil {
t.Errorf("there should be an error")
}
if dnsRCode != test.expectedDNSCode {
t.Errorf("expected DNSRCode to be %s, got %s", test.expectedDNSCode, dnsRCode)
}
if test.inputDNS.QueryType == "NS" {
// Because there are often multiple nameservers backing a single domain, we'll only look at the suffix
if !pattern.Match(test.expectedBody, string(body)) {
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
}
} else {
if string(body) != test.expectedBody {
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
}
}
})
time.Sleep(5 * time.Millisecond)
}
}

View file

@ -15,14 +15,13 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/alerting/provider" "github.com/TwiN/gatus/v5/alerting/provider"
"github.com/TwiN/gatus/v5/config/connectivity" "github.com/TwiN/gatus/v5/config/connectivity"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/config/maintenance" "github.com/TwiN/gatus/v5/config/maintenance"
"github.com/TwiN/gatus/v5/config/remote" "github.com/TwiN/gatus/v5/config/remote"
"github.com/TwiN/gatus/v5/config/ui" "github.com/TwiN/gatus/v5/config/ui"
"github.com/TwiN/gatus/v5/config/web" "github.com/TwiN/gatus/v5/config/web"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/security" "github.com/TwiN/gatus/v5/security"
"github.com/TwiN/gatus/v5/storage" "github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/util"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -74,10 +73,10 @@ type Config struct {
Alerting *alerting.Config `yaml:"alerting,omitempty"` Alerting *alerting.Config `yaml:"alerting,omitempty"`
// Endpoints is the list of endpoints to monitor // Endpoints is the list of endpoints to monitor
Endpoints []*core.Endpoint `yaml:"endpoints,omitempty"` Endpoints []*endpoint.Endpoint `yaml:"endpoints,omitempty"`
// ExternalEndpoints is the list of all external endpoints // ExternalEndpoints is the list of all external endpoints
ExternalEndpoints []*core.ExternalEndpoint `yaml:"external-endpoints,omitempty"` ExternalEndpoints []*endpoint.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
// Storage is the configuration for how the data is stored // Storage is the configuration for how the data is stored
Storage *storage.Config `yaml:"storage,omitempty"` Storage *storage.Config `yaml:"storage,omitempty"`
@ -102,20 +101,20 @@ type Config struct {
lastFileModTime time.Time // last modification time lastFileModTime time.Time // last modification time
} }
func (config *Config) GetEndpointByKey(key string) *core.Endpoint { func (config *Config) GetEndpointByKey(key string) *endpoint.Endpoint {
for i := 0; i < len(config.Endpoints); i++ { for i := 0; i < len(config.Endpoints); i++ {
ep := config.Endpoints[i] ep := config.Endpoints[i]
if util.ConvertGroupAndEndpointNameToKey(ep.Group, ep.Name) == key { if ep.Key() == key {
return ep return ep
} }
} }
return nil return nil
} }
func (config *Config) GetExternalEndpointByKey(key string) *core.ExternalEndpoint { func (config *Config) GetExternalEndpointByKey(key string) *endpoint.ExternalEndpoint {
for i := 0; i < len(config.ExternalEndpoints); i++ { for i := 0; i < len(config.ExternalEndpoints); i++ {
ee := config.ExternalEndpoints[i] ee := config.ExternalEndpoints[i]
if util.ConvertGroupAndEndpointNameToKey(ee.Group, ee.Name) == key { if ee.Key() == key {
return ee return ee
} }
} }
@ -339,12 +338,12 @@ func validateWebConfig(config *Config) error {
} }
func validateEndpointsConfig(config *Config) error { func validateEndpointsConfig(config *Config) error {
for _, endpoint := range config.Endpoints { for _, ep := range config.Endpoints {
if config.Debug { if config.Debug {
log.Printf("[config.validateEndpointsConfig] Validating endpoint '%s'", endpoint.Name) log.Printf("[config.validateEndpointsConfig] Validating endpoint '%s'", ep.Name)
} }
if err := endpoint.ValidateAndSetDefaults(); err != nil { if err := ep.ValidateAndSetDefaults(); err != nil {
return fmt.Errorf("invalid endpoint %s: %w", endpoint.DisplayName(), err) return fmt.Errorf("invalid endpoint %s: %w", ep.DisplayName(), err)
} }
} }
log.Printf("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints)) log.Printf("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
@ -352,12 +351,12 @@ func validateEndpointsConfig(config *Config) error {
} }
func validateExternalEndpointsConfig(config *Config) error { func validateExternalEndpointsConfig(config *Config) error {
for _, externalEndpoint := range config.ExternalEndpoints { for _, ee := range config.ExternalEndpoints {
if config.Debug { if config.Debug {
log.Printf("[config.validateExternalEndpointsConfig] Validating external endpoint '%s'", externalEndpoint.Name) log.Printf("[config.validateExternalEndpointsConfig] Validating external endpoint '%s'", ee.Name)
} }
if err := externalEndpoint.ValidateAndSetDefaults(); err != nil { if err := ee.ValidateAndSetDefaults(); err != nil {
return fmt.Errorf("invalid external endpoint %s: %w", externalEndpoint.DisplayName(), err) return fmt.Errorf("invalid external endpoint %s: %w", ee.DisplayName(), err)
} }
} }
log.Printf("[config.validateExternalEndpointsConfig] Validated %d external endpoints", len(config.ExternalEndpoints)) log.Printf("[config.validateExternalEndpointsConfig] Validated %d external endpoints", len(config.ExternalEndpoints))
@ -381,9 +380,9 @@ func validateSecurityConfig(config *Config) error {
// validateAlertingConfig validates the alerting configuration // validateAlertingConfig validates the alerting configuration
// Note that the alerting configuration has to be validated before the endpoint configuration, because the default alert // Note that the alerting configuration has to be validated before the endpoint configuration, because the default alert
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before core.Endpoint.ValidateAndSetDefaults() // returned by provider.AlertProvider.GetDefaultAlert() must be parsed before endpoint.Endpoint.ValidateAndSetDefaults()
// sets the default alert values when none are set. // sets the default alert values when none are set.
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.Endpoint, debug bool) { func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoint.Endpoint, debug bool) {
if alertingConfig == nil { if alertingConfig == nil {
log.Printf("[config.validateAlertingConfig] Alerting is not configured") log.Printf("[config.validateAlertingConfig] Alerting is not configured")
return return
@ -417,11 +416,11 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
if alertProvider.IsValid() { if alertProvider.IsValid() {
// Parse alerts with the provider's default alert // Parse alerts with the provider's default alert
if alertProvider.GetDefaultAlert() != nil { if alertProvider.GetDefaultAlert() != nil {
for _, endpoint := range endpoints { for _, ep := range endpoints {
for alertIndex, endpointAlert := range endpoint.Alerts { for alertIndex, endpointAlert := range ep.Alerts {
if alertType == endpointAlert.Type { if alertType == endpointAlert.Type {
if debug { if debug {
log.Printf("[config.validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in endpoint=%s", alertIndex, alertType, endpoint.Name) log.Printf("[config.validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in endpoint=%s", alertIndex, alertType, ep.Name)
} }
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert) provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
} }

View file

@ -29,8 +29,8 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/telegram" "github.com/TwiN/gatus/v5/alerting/provider/telegram"
"github.com/TwiN/gatus/v5/alerting/provider/twilio" "github.com/TwiN/gatus/v5/alerting/provider/twilio"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/config/web" "github.com/TwiN/gatus/v5/config/web"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/storage" "github.com/TwiN/gatus/v5/storage"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -65,7 +65,7 @@ func TestLoadConfiguration(t *testing.T) {
endpoints: endpoints:
- name: website`, - name: website`,
}, },
expectedError: core.ErrEndpointWithNoURL, expectedError: endpoint.ErrEndpointWithNoURL,
}, },
{ {
name: "config-file-with-endpoint-that-has-no-conditions", name: "config-file-with-endpoint-that-has-no-conditions",
@ -76,7 +76,7 @@ endpoints:
- name: website - name: website
url: https://twin.sh/health`, url: https://twin.sh/health`,
}, },
expectedError: core.ErrEndpointWithNoCondition, expectedError: endpoint.ErrEndpointWithNoCondition,
}, },
{ {
name: "config-file", name: "config-file",
@ -90,11 +90,11 @@ endpoints:
- "[STATUS] == 200"`, - "[STATUS] == 200"`,
}, },
expectedConfig: &Config{ expectedConfig: &Config{
Endpoints: []*core.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
Name: "website", Name: "website",
URL: "https://twin.sh/health", URL: "https://twin.sh/health",
Conditions: []core.Condition{"[STATUS] == 200"}, Conditions: []endpoint.Condition{"[STATUS] == 200"},
}, },
}, },
}, },
@ -136,21 +136,21 @@ endpoints:
- "[BODY].status == UP"`, - "[BODY].status == UP"`,
}, },
expectedConfig: &Config{ expectedConfig: &Config{
Endpoints: []*core.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
Name: "one", Name: "one",
URL: "https://example.com", URL: "https://example.com",
Conditions: []core.Condition{"[CONNECTED] == true", "[STATUS] == 200"}, Conditions: []endpoint.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
}, },
{ {
Name: "two", Name: "two",
URL: "https://example.org", URL: "https://example.org",
Conditions: []core.Condition{"len([BODY]) > 0"}, Conditions: []endpoint.Condition{"len([BODY]) > 0"},
}, },
{ {
Name: "three", Name: "three",
URL: "https://twin.sh/health", URL: "https://twin.sh/health",
Conditions: []core.Condition{"[STATUS] == 200", "[BODY].status == UP"}, Conditions: []endpoint.Condition{"[STATUS] == 200", "[BODY].status == UP"},
}, },
}, },
}, },
@ -192,17 +192,17 @@ endpoints:
Discord: &discord.AlertProvider{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"}, Discord: &discord.AlertProvider{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"},
Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz"}, Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz"},
}, },
Endpoints: []*core.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
Name: "example", Name: "example",
URL: "https://example.org", URL: "https://example.org",
Interval: 5 * time.Second, Interval: 5 * time.Second,
Conditions: []core.Condition{"[STATUS] == 200"}, Conditions: []endpoint.Condition{"[STATUS] == 200"},
}, },
{ {
Name: "frontend", Name: "frontend",
URL: "https://example.com", URL: "https://example.com",
Conditions: []core.Condition{"[STATUS] == 200"}, Conditions: []endpoint.Condition{"[STATUS] == 200"},
}, },
}, },
}, },
@ -689,8 +689,8 @@ endpoints:
if config.Endpoints[0].Interval != 60*time.Second { if config.Endpoints[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
} }
if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != core.GatusUserAgent { if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != endpoint.GatusUserAgent {
t.Errorf("User-Agent should've been %s because it's the default value, got %s", core.GatusUserAgent, userAgent) t.Errorf("User-Agent should've been %s because it's the default value, got %s", endpoint.GatusUserAgent, userAgent)
} }
} }

View file

@ -1,4 +1,4 @@
package core package endpoint
import ( import (
"errors" "errors"

View file

@ -1,4 +1,4 @@
package core package endpoint
import ( import (
"errors" "errors"

View file

@ -1,4 +1,4 @@
package core package endpoint
import ( import (
"errors" "errors"

View file

@ -1,6 +1,8 @@
package core package endpoint
import "testing" import (
"testing"
)
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) { func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
condition := Condition("[BODY].name == any(john.doe, jane.doe)") condition := Condition("[BODY].name == any(john.doe, jane.doe)")

View file

@ -1,4 +1,4 @@
package core package endpoint
// ConditionResult result of a Condition // ConditionResult result of a Condition
type ConditionResult struct { type ConditionResult struct {

View file

@ -1,4 +1,4 @@
package core package endpoint
import ( import (
"errors" "errors"

View 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
}

View 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...")
}
}

View file

@ -1,4 +1,4 @@
package core package endpoint
import ( import (
"bytes" "bytes"
@ -15,12 +15,13 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core/ui" "github.com/TwiN/gatus/v5/config/endpoint/dns"
"github.com/TwiN/gatus/v5/util" sshconfig "github.com/TwiN/gatus/v5/config/endpoint/ssh"
"github.com/TwiN/gatus/v5/config/endpoint/ui"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type EndpointType string type Type string
const ( const (
// HostHeader is the name of the header used to specify the host // HostHeader is the name of the header used to specify the host
@ -35,17 +36,17 @@ const (
// GatusUserAgent is the default user agent that Gatus uses to send requests. // GatusUserAgent is the default user agent that Gatus uses to send requests.
GatusUserAgent = "Gatus/1.0" GatusUserAgent = "Gatus/1.0"
EndpointTypeDNS EndpointType = "DNS" TypeDNS Type = "DNS"
EndpointTypeTCP EndpointType = "TCP" TypeTCP Type = "TCP"
EndpointTypeSCTP EndpointType = "SCTP" TypeSCTP Type = "SCTP"
EndpointTypeUDP EndpointType = "UDP" TypeUDP Type = "UDP"
EndpointTypeICMP EndpointType = "ICMP" TypeICMP Type = "ICMP"
EndpointTypeSTARTTLS EndpointType = "STARTTLS" TypeSTARTTLS Type = "STARTTLS"
EndpointTypeTLS EndpointType = "TLS" TypeTLS Type = "TLS"
EndpointTypeHTTP EndpointType = "HTTP" TypeHTTP Type = "HTTP"
EndpointTypeWS EndpointType = "WEBSOCKET" TypeWS Type = "WEBSOCKET"
EndpointTypeSSH EndpointType = "SSH" TypeSSH Type = "SSH"
EndpointTypeUNKNOWN EndpointType = "UNKNOWN" TypeUNKNOWN Type = "UNKNOWN"
) )
var ( var (
@ -82,12 +83,6 @@ type Endpoint struct {
// URL to send the request to // URL to send the request to
URL string `yaml:"url"` URL string `yaml:"url"`
// DNS is the configuration of DNS monitoring
DNS *DNS `yaml:"dns,omitempty"`
// SSH is the configuration of SSH monitoring.
SSH *SSH `yaml:"ssh,omitempty"`
// Method of the request made to the url of the endpoint // Method of the request made to the url of the endpoint
Method string `yaml:"method,omitempty"` Method string `yaml:"method,omitempty"`
@ -109,6 +104,12 @@ type Endpoint struct {
// Alerts is the alerting configuration for the endpoint in case of failure // Alerts is the alerting configuration for the endpoint in case of failure
Alerts []*alert.Alert `yaml:"alerts,omitempty"` Alerts []*alert.Alert `yaml:"alerts,omitempty"`
// DNSConfig is the configuration for DNS monitoring
DNSConfig *dns.Config `yaml:"dns,omitempty"`
// SSH is the configuration for SSH monitoring
SSHConfig *sshconfig.Config `yaml:"ssh,omitempty"`
// ClientConfig is the configuration of the client used to communicate with the endpoint's target // ClientConfig is the configuration of the client used to communicate with the endpoint's target
ClientConfig *client.Config `yaml:"client,omitempty"` ClientConfig *client.Config `yaml:"client,omitempty"`
@ -123,103 +124,103 @@ type Endpoint struct {
} }
// IsEnabled returns whether the endpoint is enabled or not // IsEnabled returns whether the endpoint is enabled or not
func (endpoint *Endpoint) IsEnabled() bool { func (e *Endpoint) IsEnabled() bool {
if endpoint.Enabled == nil { if e.Enabled == nil {
return true return true
} }
return *endpoint.Enabled return *e.Enabled
} }
// Type returns the endpoint type // Type returns the endpoint type
func (endpoint *Endpoint) Type() EndpointType { func (e *Endpoint) Type() Type {
switch { switch {
case endpoint.DNS != nil: case e.DNSConfig != nil:
return EndpointTypeDNS return TypeDNS
case strings.HasPrefix(endpoint.URL, "tcp://"): case strings.HasPrefix(e.URL, "tcp://"):
return EndpointTypeTCP return TypeTCP
case strings.HasPrefix(endpoint.URL, "sctp://"): case strings.HasPrefix(e.URL, "sctp://"):
return EndpointTypeSCTP return TypeSCTP
case strings.HasPrefix(endpoint.URL, "udp://"): case strings.HasPrefix(e.URL, "udp://"):
return EndpointTypeUDP return TypeUDP
case strings.HasPrefix(endpoint.URL, "icmp://"): case strings.HasPrefix(e.URL, "icmp://"):
return EndpointTypeICMP return TypeICMP
case strings.HasPrefix(endpoint.URL, "starttls://"): case strings.HasPrefix(e.URL, "starttls://"):
return EndpointTypeSTARTTLS return TypeSTARTTLS
case strings.HasPrefix(endpoint.URL, "tls://"): case strings.HasPrefix(e.URL, "tls://"):
return EndpointTypeTLS return TypeTLS
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"): case strings.HasPrefix(e.URL, "http://") || strings.HasPrefix(e.URL, "https://"):
return EndpointTypeHTTP return TypeHTTP
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"): case strings.HasPrefix(e.URL, "ws://") || strings.HasPrefix(e.URL, "wss://"):
return EndpointTypeWS return TypeWS
case strings.HasPrefix(endpoint.URL, "ssh://"): case strings.HasPrefix(e.URL, "ssh://"):
return EndpointTypeSSH return TypeSSH
default: default:
return EndpointTypeUNKNOWN return TypeUNKNOWN
} }
} }
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one // ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
func (endpoint *Endpoint) ValidateAndSetDefaults() error { func (e *Endpoint) ValidateAndSetDefaults() error {
if err := validateEndpointNameGroupAndAlerts(endpoint.Name, endpoint.Group, endpoint.Alerts); err != nil { if err := validateEndpointNameGroupAndAlerts(e.Name, e.Group, e.Alerts); err != nil {
return err return err
} }
if len(endpoint.URL) == 0 { if len(e.URL) == 0 {
return ErrEndpointWithNoURL return ErrEndpointWithNoURL
} }
if endpoint.ClientConfig == nil { if e.ClientConfig == nil {
endpoint.ClientConfig = client.GetDefaultConfig() e.ClientConfig = client.GetDefaultConfig()
} else { } else {
if err := endpoint.ClientConfig.ValidateAndSetDefaults(); err != nil { if err := e.ClientConfig.ValidateAndSetDefaults(); err != nil {
return err return err
} }
} }
if endpoint.UIConfig == nil { if e.UIConfig == nil {
endpoint.UIConfig = ui.GetDefaultConfig() e.UIConfig = ui.GetDefaultConfig()
} else { } else {
if err := endpoint.UIConfig.ValidateAndSetDefaults(); err != nil { if err := e.UIConfig.ValidateAndSetDefaults(); err != nil {
return err return err
} }
} }
if endpoint.Interval == 0 { if e.Interval == 0 {
endpoint.Interval = 1 * time.Minute e.Interval = 1 * time.Minute
} }
if len(endpoint.Method) == 0 { if len(e.Method) == 0 {
endpoint.Method = http.MethodGet e.Method = http.MethodGet
} }
if len(endpoint.Headers) == 0 { if len(e.Headers) == 0 {
endpoint.Headers = make(map[string]string) e.Headers = make(map[string]string)
} }
// Automatically add user agent header if there isn't one specified in the endpoint configuration // Automatically add user agent header if there isn't one specified in the endpoint configuration
if _, userAgentHeaderExists := endpoint.Headers[UserAgentHeader]; !userAgentHeaderExists { if _, userAgentHeaderExists := e.Headers[UserAgentHeader]; !userAgentHeaderExists {
endpoint.Headers[UserAgentHeader] = GatusUserAgent e.Headers[UserAgentHeader] = GatusUserAgent
} }
// Automatically add "Content-Type: application/json" header if there's no Content-Type set // Automatically add "Content-Type: application/json" header if there's no Content-Type set
// and endpoint.GraphQL is set to true // and endpoint.GraphQL is set to true
if _, contentTypeHeaderExists := endpoint.Headers[ContentTypeHeader]; !contentTypeHeaderExists && endpoint.GraphQL { if _, contentTypeHeaderExists := e.Headers[ContentTypeHeader]; !contentTypeHeaderExists && e.GraphQL {
endpoint.Headers[ContentTypeHeader] = "application/json" e.Headers[ContentTypeHeader] = "application/json"
} }
if len(endpoint.Conditions) == 0 { if len(e.Conditions) == 0 {
return ErrEndpointWithNoCondition return ErrEndpointWithNoCondition
} }
for _, c := range endpoint.Conditions { for _, c := range e.Conditions {
if endpoint.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() { if e.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
} }
if err := c.Validate(); err != nil { if err := c.Validate(); err != nil {
return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err) return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err)
} }
} }
if endpoint.DNS != nil { if e.DNSConfig != nil {
return endpoint.DNS.validateAndSetDefault() return e.DNSConfig.ValidateAndSetDefault()
} }
if endpoint.SSH != nil { if e.SSHConfig != nil {
return endpoint.SSH.validate() return e.SSHConfig.Validate()
} }
if endpoint.Type() == EndpointTypeUNKNOWN { if e.Type() == TypeUNKNOWN {
return ErrUnknownEndpointType return ErrUnknownEndpointType
} }
// Make sure that the request can be created // Make sure that the request can be created
_, err := http.NewRequest(endpoint.Method, endpoint.URL, bytes.NewBuffer([]byte(endpoint.Body))) _, err := http.NewRequest(e.Method, e.URL, bytes.NewBuffer([]byte(e.Body)))
if err != nil { if err != nil {
return err return err
} }
@ -227,35 +228,35 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
} }
// DisplayName returns an identifier made up of the Name and, if not empty, the Group. // DisplayName returns an identifier made up of the Name and, if not empty, the Group.
func (endpoint *Endpoint) DisplayName() string { func (e *Endpoint) DisplayName() string {
if len(endpoint.Group) > 0 { if len(e.Group) > 0 {
return endpoint.Group + "/" + endpoint.Name return e.Group + "/" + e.Name
} }
return endpoint.Name return e.Name
} }
// Key returns the unique key for the Endpoint // Key returns the unique key for the Endpoint
func (endpoint *Endpoint) Key() string { func (e *Endpoint) Key() string {
return util.ConvertGroupAndEndpointNameToKey(endpoint.Group, endpoint.Name) return ConvertGroupAndEndpointNameToKey(e.Group, e.Name)
} }
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors // Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
// on configuration reload. // on configuration reload.
// More context on https://github.com/TwiN/gatus/issues/536 // More context on https://github.com/TwiN/gatus/issues/536
func (endpoint *Endpoint) Close() { func (e *Endpoint) Close() {
if endpoint.Type() == EndpointTypeHTTP { if e.Type() == TypeHTTP {
client.GetHTTPClient(endpoint.ClientConfig).CloseIdleConnections() client.GetHTTPClient(e.ClientConfig).CloseIdleConnections()
} }
} }
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint. // EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
func (endpoint *Endpoint) EvaluateHealth() *Result { func (e *Endpoint) EvaluateHealth() *Result {
result := &Result{Success: true, Errors: []string{}} result := &Result{Success: true, Errors: []string{}}
// Parse or extract hostname from URL // Parse or extract hostname from URL
if endpoint.DNS != nil { if e.DNSConfig != nil {
result.Hostname = strings.TrimSuffix(endpoint.URL, ":53") result.Hostname = strings.TrimSuffix(e.URL, ":53")
} else { } else {
urlObject, err := url.Parse(endpoint.URL) urlObject, err := url.Parse(e.URL)
if err != nil { if err != nil {
result.AddError(err.Error()) result.AddError(err.Error())
} else { } else {
@ -263,11 +264,11 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
} }
} }
// Retrieve IP if necessary // Retrieve IP if necessary
if endpoint.needsToRetrieveIP() { if e.needsToRetrieveIP() {
endpoint.getIP(result) e.getIP(result)
} }
// Retrieve domain expiration if necessary // Retrieve domain expiration if necessary
if endpoint.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 { if e.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
var err error var err error
if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil { if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil {
result.AddError(err.Error()) result.AddError(err.Error())
@ -275,37 +276,37 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
} }
// Call the endpoint (if there's no errors) // Call the endpoint (if there's no errors)
if len(result.Errors) == 0 { if len(result.Errors) == 0 {
endpoint.call(result) e.call(result)
} else { } else {
result.Success = false result.Success = false
} }
// Evaluate the conditions // Evaluate the conditions
for _, condition := range endpoint.Conditions { for _, condition := range e.Conditions {
success := condition.evaluate(result, endpoint.UIConfig.DontResolveFailedConditions) success := condition.evaluate(result, e.UIConfig.DontResolveFailedConditions)
if !success { if !success {
result.Success = false result.Success = false
} }
} }
result.Timestamp = time.Now() result.Timestamp = time.Now()
// Clean up parameters that we don't need to keep in the results // Clean up parameters that we don't need to keep in the results
if endpoint.UIConfig.HideURL { if e.UIConfig.HideURL {
for errIdx, errorString := range result.Errors { for errIdx, errorString := range result.Errors {
result.Errors[errIdx] = strings.ReplaceAll(errorString, endpoint.URL, "<redacted>") result.Errors[errIdx] = strings.ReplaceAll(errorString, e.URL, "<redacted>")
} }
} }
if endpoint.UIConfig.HideHostname { if e.UIConfig.HideHostname {
for errIdx, errorString := range result.Errors { for errIdx, errorString := range result.Errors {
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>") result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
} }
result.Hostname = "" result.Hostname = ""
} }
if endpoint.UIConfig.HideConditions { if e.UIConfig.HideConditions {
result.ConditionResults = nil result.ConditionResults = nil
} }
return result return result
} }
func (endpoint *Endpoint) getIP(result *Result) { func (e *Endpoint) getIP(result *Result) {
if ips, err := net.LookupIP(result.Hostname); err != nil { if ips, err := net.LookupIP(result.Hostname); err != nil {
result.AddError(err.Error()) result.AddError(err.Error())
return return
@ -314,24 +315,28 @@ func (endpoint *Endpoint) getIP(result *Result) {
} }
} }
func (endpoint *Endpoint) call(result *Result) { func (e *Endpoint) call(result *Result) {
var request *http.Request var request *http.Request
var response *http.Response var response *http.Response
var err error var err error
var certificate *x509.Certificate var certificate *x509.Certificate
endpointType := endpoint.Type() endpointType := e.Type()
if endpointType == EndpointTypeHTTP { if endpointType == TypeHTTP {
request = endpoint.buildHTTPRequest() request = e.buildHTTPRequest()
} }
startTime := time.Now() startTime := time.Now()
if endpointType == EndpointTypeDNS { if endpointType == TypeDNS {
endpoint.DNS.query(endpoint.URL, result) result.Connected, result.DNSRCode, result.Body, err = client.QueryDNS(e.DNSConfig.QueryType, e.DNSConfig.QueryName, e.URL)
if err != nil {
result.AddError(err.Error())
return
}
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
} else if endpointType == EndpointTypeSTARTTLS || endpointType == EndpointTypeTLS { } else if endpointType == TypeSTARTTLS || endpointType == TypeTLS {
if endpointType == EndpointTypeSTARTTLS { if endpointType == TypeSTARTTLS {
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(endpoint.URL, "starttls://"), endpoint.ClientConfig) result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(e.URL, "starttls://"), e.ClientConfig)
} else { } else {
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(endpoint.URL, "tls://"), endpoint.ClientConfig) result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(e.URL, "tls://"), e.ClientConfig)
} }
if err != nil { if err != nil {
result.AddError(err.Error()) result.AddError(err.Error())
@ -339,39 +344,39 @@ func (endpoint *Endpoint) call(result *Result) {
} }
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
result.CertificateExpiration = time.Until(certificate.NotAfter) result.CertificateExpiration = time.Until(certificate.NotAfter)
} else if endpointType == EndpointTypeTCP { } else if endpointType == TypeTCP {
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig) result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(e.URL, "tcp://"), e.ClientConfig)
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
} else if endpointType == EndpointTypeUDP { } else if endpointType == TypeUDP {
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(endpoint.URL, "udp://"), endpoint.ClientConfig) result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(e.URL, "udp://"), e.ClientConfig)
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
} else if endpointType == EndpointTypeSCTP { } else if endpointType == TypeSCTP {
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(endpoint.URL, "sctp://"), endpoint.ClientConfig) result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(e.URL, "sctp://"), e.ClientConfig)
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
} else if endpointType == EndpointTypeICMP { } else if endpointType == TypeICMP {
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig) result.Connected, result.Duration = client.Ping(strings.TrimPrefix(e.URL, "icmp://"), e.ClientConfig)
} else if endpointType == EndpointTypeWS { } else if endpointType == TypeWS {
result.Connected, result.Body, err = client.QueryWebSocket(endpoint.URL, endpoint.Body, endpoint.ClientConfig) result.Connected, result.Body, err = client.QueryWebSocket(e.URL, e.Body, e.ClientConfig)
if err != nil { if err != nil {
result.AddError(err.Error()) result.AddError(err.Error())
return return
} }
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
} else if endpointType == EndpointTypeSSH { } else if endpointType == TypeSSH {
var cli *ssh.Client var cli *ssh.Client
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(endpoint.URL, "ssh://"), endpoint.SSH.Username, endpoint.SSH.Password, endpoint.ClientConfig) result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(e.URL, "ssh://"), e.SSHConfig.Username, e.SSHConfig.Password, e.ClientConfig)
if err != nil { if err != nil {
result.AddError(err.Error()) result.AddError(err.Error())
return return
} }
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, endpoint.Body, endpoint.ClientConfig) result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, e.Body, e.ClientConfig)
if err != nil { if err != nil {
result.AddError(err.Error()) result.AddError(err.Error())
return return
} }
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
} else { } else {
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request) response, err = client.GetHTTPClient(e.ClientConfig).Do(request)
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
if err != nil { if err != nil {
result.AddError(err.Error()) result.AddError(err.Error())
@ -385,7 +390,7 @@ func (endpoint *Endpoint) call(result *Result) {
result.HTTPStatus = response.StatusCode result.HTTPStatus = response.StatusCode
result.Connected = response.StatusCode > 0 result.Connected = response.StatusCode > 0
// Only read the Body if there's a condition that uses the BodyPlaceholder // Only read the Body if there's a condition that uses the BodyPlaceholder
if endpoint.needsToReadBody() { if e.needsToReadBody() {
result.Body, err = io.ReadAll(response.Body) result.Body, err = io.ReadAll(response.Body)
if err != nil { if err != nil {
result.AddError("error reading response body:" + err.Error()) result.AddError("error reading response body:" + err.Error())
@ -394,19 +399,19 @@ func (endpoint *Endpoint) call(result *Result) {
} }
} }
func (endpoint *Endpoint) buildHTTPRequest() *http.Request { func (e *Endpoint) buildHTTPRequest() *http.Request {
var bodyBuffer *bytes.Buffer var bodyBuffer *bytes.Buffer
if endpoint.GraphQL { if e.GraphQL {
graphQlBody := map[string]string{ graphQlBody := map[string]string{
"query": endpoint.Body, "query": e.Body,
} }
body, _ := json.Marshal(graphQlBody) body, _ := json.Marshal(graphQlBody)
bodyBuffer = bytes.NewBuffer(body) bodyBuffer = bytes.NewBuffer(body)
} else { } else {
bodyBuffer = bytes.NewBuffer([]byte(endpoint.Body)) bodyBuffer = bytes.NewBuffer([]byte(e.Body))
} }
request, _ := http.NewRequest(endpoint.Method, endpoint.URL, bodyBuffer) request, _ := http.NewRequest(e.Method, e.URL, bodyBuffer)
for k, v := range endpoint.Headers { for k, v := range e.Headers {
request.Header.Set(k, v) request.Header.Set(k, v)
if k == HostHeader { if k == HostHeader {
request.Host = v request.Host = v
@ -416,8 +421,8 @@ func (endpoint *Endpoint) buildHTTPRequest() *http.Request {
} }
// needsToReadBody checks if there's any condition that requires the response Body to be read // needsToReadBody checks if there's any condition that requires the response Body to be read
func (endpoint *Endpoint) needsToReadBody() bool { func (e *Endpoint) needsToReadBody() bool {
for _, condition := range endpoint.Conditions { for _, condition := range e.Conditions {
if condition.hasBodyPlaceholder() { if condition.hasBodyPlaceholder() {
return true return true
} }
@ -426,8 +431,8 @@ func (endpoint *Endpoint) needsToReadBody() bool {
} }
// needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed // needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed
func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool { func (e *Endpoint) needsToRetrieveDomainExpiration() bool {
for _, condition := range endpoint.Conditions { for _, condition := range e.Conditions {
if condition.hasDomainExpirationPlaceholder() { if condition.hasDomainExpirationPlaceholder() {
return true return true
} }
@ -436,8 +441,8 @@ func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
} }
// needsToRetrieveIP checks if there's any condition that requires an IP lookup // needsToRetrieveIP checks if there's any condition that requires an IP lookup
func (endpoint *Endpoint) needsToRetrieveIP() bool { func (e *Endpoint) needsToRetrieveIP() bool {
for _, condition := range endpoint.Conditions { for _, condition := range e.Conditions {
if condition.hasIPPlaceholder() { if condition.hasIPPlaceholder() {
return true return true
} }

View file

@ -1,4 +1,4 @@
package core package endpoint
import ( import (
"bytes" "bytes"
@ -13,7 +13,9 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client" "github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core/ui" "github.com/TwiN/gatus/v5/config/endpoint/dns"
"github.com/TwiN/gatus/v5/config/endpoint/ssh"
"github.com/TwiN/gatus/v5/config/endpoint/ui"
"github.com/TwiN/gatus/v5/test" "github.com/TwiN/gatus/v5/test"
) )
@ -279,105 +281,105 @@ func TestEndpoint_IsEnabled(t *testing.T) {
func TestEndpoint_Type(t *testing.T) { func TestEndpoint_Type(t *testing.T) {
type args struct { type args struct {
URL string URL string
DNS *DNS DNS *dns.Config
SSH *SSH SSH *ssh.Config
} }
tests := []struct { tests := []struct {
args args args args
want EndpointType want Type
}{ }{
{ {
args: args{ args: args{
URL: "8.8.8.8", URL: "8.8.8.8",
DNS: &DNS{ DNS: &dns.Config{
QueryType: "A", QueryType: "A",
QueryName: "example.com", QueryName: "example.com",
}, },
}, },
want: EndpointTypeDNS, want: TypeDNS,
}, },
{ {
args: args{ args: args{
URL: "tcp://127.0.0.1:6379", URL: "tcp://127.0.0.1:6379",
}, },
want: EndpointTypeTCP, want: TypeTCP,
}, },
{ {
args: args{ args: args{
URL: "icmp://example.com", URL: "icmp://example.com",
}, },
want: EndpointTypeICMP, want: TypeICMP,
}, },
{ {
args: args{ args: args{
URL: "sctp://example.com", URL: "sctp://example.com",
}, },
want: EndpointTypeSCTP, want: TypeSCTP,
}, },
{ {
args: args{ args: args{
URL: "udp://example.com", URL: "udp://example.com",
}, },
want: EndpointTypeUDP, want: TypeUDP,
}, },
{ {
args: args{ args: args{
URL: "starttls://smtp.gmail.com:587", URL: "starttls://smtp.gmail.com:587",
}, },
want: EndpointTypeSTARTTLS, want: TypeSTARTTLS,
}, },
{ {
args: args{ args: args{
URL: "tls://example.com:443", URL: "tls://example.com:443",
}, },
want: EndpointTypeTLS, want: TypeTLS,
}, },
{ {
args: args{ args: args{
URL: "https://twin.sh/health", URL: "https://twin.sh/health",
}, },
want: EndpointTypeHTTP, want: TypeHTTP,
}, },
{ {
args: args{ args: args{
URL: "wss://example.com/", URL: "wss://example.com/",
}, },
want: EndpointTypeWS, want: TypeWS,
}, },
{ {
args: args{ args: args{
URL: "ws://example.com/", URL: "ws://example.com/",
}, },
want: EndpointTypeWS, want: TypeWS,
}, },
{ {
args: args{ args: args{
URL: "ssh://example.com:22", URL: "ssh://example.com:22",
SSH: &SSH{ SSH: &ssh.Config{
Username: "root", Username: "root",
Password: "password", Password: "password",
}, },
}, },
want: EndpointTypeSSH, want: TypeSSH,
}, },
{ {
args: args{ args: args{
URL: "invalid://example.org", URL: "invalid://example.org",
}, },
want: EndpointTypeUNKNOWN, want: TypeUNKNOWN,
}, },
{ {
args: args{ args: args{
URL: "no-scheme", URL: "no-scheme",
}, },
want: EndpointTypeUNKNOWN, want: TypeUNKNOWN,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(string(tt.want), func(t *testing.T) { t.Run(string(tt.want), func(t *testing.T) {
endpoint := Endpoint{ endpoint := Endpoint{
URL: tt.args.URL, URL: tt.args.URL,
DNS: tt.args.DNS, DNSConfig: tt.args.DNS,
} }
if got := endpoint.Type(); got != tt.want { if got := endpoint.Type(); got != tt.want {
t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want) t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want)
@ -477,7 +479,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
endpoint := &Endpoint{ endpoint := &Endpoint{
Name: "dns-test", Name: "dns-test",
URL: "https://example.com", URL: "https://example.com",
DNS: &DNS{ DNSConfig: &dns.Config{
QueryType: "A", QueryType: "A",
QueryName: "example.com", QueryName: "example.com",
}, },
@ -487,7 +489,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
if err != nil { if err != nil {
t.Error("did not expect an error, got", err) t.Error("did not expect an error, got", err)
} }
if endpoint.DNS.QueryName != "example.com." { if endpoint.DNSConfig.QueryName != "example.com." {
t.Error("Endpoint.dns.query-name should be formatted with . suffix") t.Error("Endpoint.dns.query-name should be formatted with . suffix")
} }
} }
@ -503,13 +505,13 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
name: "fail when has no user", name: "fail when has no user",
username: "", username: "",
password: "password", password: "password",
expectedErr: ErrEndpointWithoutSSHUsername, expectedErr: ssh.ErrEndpointWithoutSSHUsername,
}, },
{ {
name: "fail when has no password", name: "fail when has no password",
username: "username", username: "username",
password: "", password: "",
expectedErr: ErrEndpointWithoutSSHPassword, expectedErr: ssh.ErrEndpointWithoutSSHPassword,
}, },
{ {
name: "success when all fields are set", name: "success when all fields are set",
@ -524,7 +526,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
endpoint := &Endpoint{ endpoint := &Endpoint{
Name: "ssh-test", Name: "ssh-test",
URL: "https://example.com", URL: "https://example.com",
SSH: &SSH{ SSHConfig: &ssh.Config{
Username: scenario.username, Username: scenario.username,
Password: scenario.password, Password: scenario.password,
}, },
@ -763,7 +765,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
endpoint := Endpoint{ endpoint := Endpoint{
Name: "example", Name: "example",
URL: "8.8.8.8", URL: "8.8.8.8",
DNS: &DNS{ DNSConfig: &dns.Config{
QueryType: "A", QueryType: "A",
QueryName: "example.com.", QueryName: "example.com.",
}, },
@ -786,7 +788,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
} }
func TestIntegrationEvaluateHealthForSSH(t *testing.T) { func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
tests := []struct { scenarios := []struct {
name string name string
endpoint Endpoint endpoint Endpoint
conditions []Condition conditions []Condition
@ -797,9 +799,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
endpoint: Endpoint{ endpoint: Endpoint{
Name: "ssh-success", Name: "ssh-success",
URL: "ssh://localhost", URL: "ssh://localhost",
SSH: &SSH{ SSHConfig: &ssh.Config{
Username: "test", Username: "scenario",
Password: "test", Password: "scenario",
}, },
Body: "{ \"command\": \"uptime\" }", Body: "{ \"command\": \"uptime\" }",
}, },
@ -811,9 +813,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
endpoint: Endpoint{ endpoint: Endpoint{
Name: "ssh-failure", Name: "ssh-failure",
URL: "ssh://localhost", URL: "ssh://localhost",
SSH: &SSH{ SSHConfig: &ssh.Config{
Username: "test", Username: "scenario",
Password: "test", Password: "scenario",
}, },
Body: "{ \"command\": \"uptime\" }", Body: "{ \"command\": \"uptime\" }",
}, },
@ -822,13 +824,13 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
}, },
} }
for _, test := range tests { for _, scenario := range scenarios {
t.Run(test.name, func(t *testing.T) { t.Run(scenario.name, func(t *testing.T) {
test.endpoint.ValidateAndSetDefaults() scenario.endpoint.ValidateAndSetDefaults()
test.endpoint.Conditions = test.conditions scenario.endpoint.Conditions = scenario.conditions
result := test.endpoint.EvaluateHealth() result := scenario.endpoint.EvaluateHealth()
if result.Success != test.success { if result.Success != scenario.success {
t.Errorf("Expected success to be %v, but was %v", test.success, result.Success) t.Errorf("Expected success to be %v, but was %v", scenario.success, result.Success)
} }
}) })
} }

View file

@ -1,6 +1,8 @@
package core package endpoint
import "time" import (
"time"
)
// Event is something that happens at a specific time // Event is something that happens at a specific time
type Event struct { type Event struct {

View file

@ -1,6 +1,8 @@
package core package endpoint
import "testing" import (
"testing"
)
func TestNewEventFromResult(t *testing.T) { func TestNewEventFromResult(t *testing.T) {
if event := NewEventFromResult(&Result{Success: true}); event.Type != EventHealthy { if event := NewEventFromResult(&Result{Success: true}); event.Type != EventHealthy {

View file

@ -1,10 +1,9 @@
package core package endpoint
import ( import (
"errors" "errors"
"github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/util"
) )
var ( var (
@ -72,7 +71,7 @@ func (externalEndpoint *ExternalEndpoint) DisplayName() string {
// Key returns the unique key for the Endpoint // Key returns the unique key for the Endpoint
func (externalEndpoint *ExternalEndpoint) Key() string { func (externalEndpoint *ExternalEndpoint) Key() string {
return util.ConvertGroupAndEndpointNameToKey(externalEndpoint.Group, externalEndpoint.Name) return ConvertGroupAndEndpointNameToKey(externalEndpoint.Group, externalEndpoint.Name)
} }
// ToEndpoint converts the ExternalEndpoint to an Endpoint // ToEndpoint converts the ExternalEndpoint to an Endpoint

View file

@ -1,4 +1,4 @@
package core package endpoint
import ( import (
"testing" "testing"

View file

@ -1,4 +1,4 @@
package util package endpoint
import "strings" import "strings"

View file

@ -1,4 +1,4 @@
package util package endpoint
import ( import (
"testing" "testing"

View file

@ -1,4 +1,4 @@
package util package endpoint
import "testing" import "testing"

View file

@ -1,4 +1,4 @@
package core package endpoint
import ( import (
"time" "time"

View file

@ -1,4 +1,4 @@
package core package endpoint
import ( import (
"testing" "testing"

View file

@ -1,4 +1,4 @@
package core package ssh
import ( import (
"errors" "errors"
@ -12,17 +12,17 @@ var (
ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each SSH endpoint") ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each SSH endpoint")
) )
type SSH struct { type Config struct {
Username string `yaml:"username,omitempty"` Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"` Password string `yaml:"password,omitempty"`
} }
// validate validates the endpoint // Validate the SSH configuration
func (s *SSH) validate() error { func (cfg *Config) Validate() error {
if len(s.Username) == 0 { if len(cfg.Username) == 0 {
return ErrEndpointWithoutSSHUsername return ErrEndpointWithoutSSHUsername
} }
if len(s.Password) == 0 { if len(cfg.Password) == 0 {
return ErrEndpointWithoutSSHPassword return ErrEndpointWithoutSSHPassword
} }
return nil return nil

View file

@ -1,4 +1,4 @@
package core package ssh
import ( import (
"errors" "errors"
@ -6,20 +6,20 @@ import (
) )
func TestSSH_validate(t *testing.T) { func TestSSH_validate(t *testing.T) {
ssh := &SSH{} cfg := &Config{}
if err := ssh.validate(); err == nil { if err := cfg.Validate(); err == nil {
t.Error("expected an error") t.Error("expected an error")
} else if !errors.Is(err, ErrEndpointWithoutSSHUsername) { } else if !errors.Is(err, ErrEndpointWithoutSSHUsername) {
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHUsername, err) t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHUsername, err)
} }
ssh.Username = "username" cfg.Username = "username"
if err := ssh.validate(); err == nil { if err := cfg.Validate(); err == nil {
t.Error("expected an error") t.Error("expected an error")
} else if !errors.Is(err, ErrEndpointWithoutSSHPassword) { } else if !errors.Is(err, ErrEndpointWithoutSSHPassword) {
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHPassword, err) t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHPassword, err)
} }
ssh.Password = "password" cfg.Password = "password"
if err := ssh.validate(); err != nil { if err := cfg.Validate(); err != nil {
t.Errorf("expected no error, got '%v'", err) t.Errorf("expected no error, got '%v'", err)
} }
} }

View file

@ -1,16 +1,14 @@
package core package endpoint
import "github.com/TwiN/gatus/v5/util" // Status contains the evaluation Results of an Endpoint
type Status struct {
// EndpointStatus contains the evaluation Results of an Endpoint
type EndpointStatus struct {
// Name of the endpoint // Name of the endpoint
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// Group the endpoint is a part of. Used for grouping multiple endpoints together on the front end. // Group the endpoint is a part of. Used for grouping multiple endpoints together on the front end.
Group string `json:"group,omitempty"` Group string `json:"group,omitempty"`
// Key is the key representing the EndpointStatus // Key of the Endpoint
Key string `json:"key"` Key string `json:"key"`
// Results is the list of endpoint evaluation results // Results is the list of endpoint evaluation results
@ -27,12 +25,12 @@ type EndpointStatus struct {
Uptime *Uptime `json:"-"` Uptime *Uptime `json:"-"`
} }
// NewEndpointStatus creates a new EndpointStatus // NewStatus creates a new Status
func NewEndpointStatus(group, name string) *EndpointStatus { func NewStatus(group, name string) *Status {
return &EndpointStatus{ return &Status{
Name: name, Name: name,
Group: group, Group: group,
Key: util.ConvertGroupAndEndpointNameToKey(group, name), Key: ConvertGroupAndEndpointNameToKey(group, name),
Results: make([]*Result, 0), Results: make([]*Result, 0),
Events: make([]*Event, 0), Events: make([]*Event, 0),
Uptime: NewUptime(), Uptime: NewUptime(),

View 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)
}
}

View file

@ -2,7 +2,7 @@ package ui
import "errors" import "errors"
// Config is the UI configuration for core.Endpoint // Config is the UI configuration for endpoint.Endpoint
type Config struct { type Config struct {
// HideConditions whether to hide the condition results on the UI // HideConditions whether to hide the condition results on the UI
HideConditions bool `yaml:"hide-conditions"` HideConditions bool `yaml:"hide-conditions"`

View file

@ -1,4 +1,4 @@
package core package endpoint
// Uptime is the struct that contains the relevant data for calculating the uptime as well as the uptime itself // Uptime is the struct that contains the relevant data for calculating the uptime as well as the uptime itself
// and some other statistics // and some other statistics

View file

@ -1 +0,0 @@
TODO: move files from core to here.

View file

@ -8,8 +8,8 @@ import (
"testing" "testing"
"github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/config/web" "github.com/TwiN/gatus/v5/config/web"
"github.com/TwiN/gatus/v5/core"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -19,7 +19,7 @@ func TestHandle(t *testing.T) {
Address: "0.0.0.0", Address: "0.0.0.0",
Port: rand.Intn(65534), Port: rand.Intn(65534),
}, },
Endpoints: []*core.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
Name: "frontend", Name: "frontend",
Group: "core", Group: "core",
@ -64,7 +64,7 @@ func TestHandleTLS(t *testing.T) {
t.Run(scenario.name, func(t *testing.T) { t.Run(scenario.name, func(t *testing.T) {
cfg := &config.Config{ cfg := &config.Config{
Web: &web.Config{Address: "0.0.0.0", Port: rand.Intn(65534), TLS: scenario.tls}, Web: &web.Config{Address: "0.0.0.0", Port: rand.Intn(65534), TLS: scenario.tls},
Endpoints: []*core.Endpoint{ Endpoints: []*endpoint.Endpoint{
{Name: "frontend", Group: "core"}, {Name: "frontend", Group: "core"},
{Name: "backend", Group: "core"}, {Name: "backend", Group: "core"},
}, },

View file

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

View file

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

View file

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

View file

@ -80,8 +80,8 @@ func initializeStorage(cfg *config.Config) {
} }
// Remove all EndpointStatus that represent endpoints which no longer exist in the configuration // Remove all EndpointStatus that represent endpoints which no longer exist in the configuration
var keys []string var keys []string
for _, endpoint := range cfg.Endpoints { for _, ep := range cfg.Endpoints {
keys = append(keys, endpoint.Key()) keys = append(keys, ep.Key())
} }
for _, externalEndpoint := range cfg.ExternalEndpoints { for _, externalEndpoint := range cfg.ExternalEndpoints {
keys = append(keys, externalEndpoint.Key()) keys = append(keys, externalEndpoint.Key())

View file

@ -3,7 +3,7 @@ package metrics
import ( import (
"strconv" "strconv"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
) )
@ -50,24 +50,24 @@ func initializePrometheusMetrics() {
// PublishMetricsForEndpoint publishes metrics for the given endpoint and its result. // PublishMetricsForEndpoint publishes metrics for the given endpoint and its result.
// These metrics will be exposed at /metrics if the metrics are enabled // These metrics will be exposed at /metrics if the metrics are enabled
func PublishMetricsForEndpoint(endpoint *core.Endpoint, result *core.Result) { func PublishMetricsForEndpoint(ep *endpoint.Endpoint, result *endpoint.Result) {
if !initializedMetrics { if !initializedMetrics {
initializePrometheusMetrics() initializePrometheusMetrics()
initializedMetrics = true initializedMetrics = true
} }
endpointType := endpoint.Type() endpointType := ep.Type()
resultTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), strconv.FormatBool(result.Success)).Inc() resultTotal.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType), strconv.FormatBool(result.Success)).Inc()
resultDurationSeconds.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Set(result.Duration.Seconds()) resultDurationSeconds.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType)).Set(result.Duration.Seconds())
if result.Connected { if result.Connected {
resultConnectedTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Inc() resultConnectedTotal.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType)).Inc()
} }
if result.DNSRCode != "" { if result.DNSRCode != "" {
resultCodeTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), result.DNSRCode).Inc() resultCodeTotal.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType), result.DNSRCode).Inc()
} }
if result.HTTPStatus != 0 { if result.HTTPStatus != 0 {
resultCodeTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), strconv.Itoa(result.HTTPStatus)).Inc() resultCodeTotal.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType), strconv.Itoa(result.HTTPStatus)).Inc()
} }
if result.CertificateExpiration != 0 { if result.CertificateExpiration != 0 {
resultCertificateExpirationSeconds.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Set(result.CertificateExpiration.Seconds()) resultCertificateExpirationSeconds.WithLabelValues(ep.Key(), ep.Group, ep.Name, string(endpointType)).Set(result.CertificateExpiration.Seconds())
} }
} }

View file

@ -5,18 +5,19 @@ import (
"testing" "testing"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/config/endpoint/dns"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil" "github.com/prometheus/client_golang/prometheus/testutil"
) )
func TestPublishMetricsForEndpoint(t *testing.T) { func TestPublishMetricsForEndpoint(t *testing.T) {
httpEndpoint := &core.Endpoint{Name: "http-ep-name", Group: "http-ep-group", URL: "https://example.org"} httpEndpoint := &endpoint.Endpoint{Name: "http-ep-name", Group: "http-ep-group", URL: "https://example.org"}
PublishMetricsForEndpoint(httpEndpoint, &core.Result{ PublishMetricsForEndpoint(httpEndpoint, &endpoint.Result{
HTTPStatus: 200, HTTPStatus: 200,
Connected: true, Connected: true,
Duration: 123 * time.Millisecond, Duration: 123 * time.Millisecond,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[STATUS] == 200", Success: true}, {Condition: "[STATUS] == 200", Success: true},
{Condition: "[CERTIFICATE_EXPIRATION] > 48h", Success: true}, {Condition: "[CERTIFICATE_EXPIRATION] > 48h", Success: true},
}, },
@ -43,11 +44,11 @@ gatus_results_total{group="http-ep-group",key="http-ep-group_http-ep-name",name=
if err != nil { if err != nil {
t.Errorf("Expected no errors but got: %v", err) t.Errorf("Expected no errors but got: %v", err)
} }
PublishMetricsForEndpoint(httpEndpoint, &core.Result{ PublishMetricsForEndpoint(httpEndpoint, &endpoint.Result{
HTTPStatus: 200, HTTPStatus: 200,
Connected: true, Connected: true,
Duration: 125 * time.Millisecond, Duration: 125 * time.Millisecond,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[STATUS] == 200", Success: true}, {Condition: "[STATUS] == 200", Success: true},
{Condition: "[CERTIFICATE_EXPIRATION] > 47h", Success: false}, {Condition: "[CERTIFICATE_EXPIRATION] > 47h", Success: false},
}, },
@ -75,15 +76,15 @@ gatus_results_total{group="http-ep-group",key="http-ep-group_http-ep-name",name=
if err != nil { if err != nil {
t.Errorf("Expected no errors but got: %v", err) t.Errorf("Expected no errors but got: %v", err)
} }
dnsEndpoint := &core.Endpoint{Name: "dns-ep-name", Group: "dns-ep-group", URL: "8.8.8.8", DNS: &core.DNS{ dnsEndpoint := &endpoint.Endpoint{Name: "dns-ep-name", Group: "dns-ep-group", URL: "8.8.8.8", DNSConfig: &dns.Config{
QueryType: "A", QueryType: "A",
QueryName: "example.com.", QueryName: "example.com.",
}} }}
PublishMetricsForEndpoint(dnsEndpoint, &core.Result{ PublishMetricsForEndpoint(dnsEndpoint, &endpoint.Result{
DNSRCode: "NOERROR", DNSRCode: "NOERROR",
Connected: true, Connected: true,
Duration: 50 * time.Millisecond, Duration: 50 * time.Millisecond,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{Condition: "[DNS_RCODE] == NOERROR", Success: true}, {Condition: "[DNS_RCODE] == NOERROR", Success: true},
}, },
Success: true, Success: true,

View file

@ -5,10 +5,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
"github.com/TwiN/gatus/v5/util"
"github.com/TwiN/gocache/v2" "github.com/TwiN/gocache/v2"
) )
@ -30,13 +29,13 @@ func NewStore() (*Store, error) {
return store, nil return store, nil
} }
// GetAllEndpointStatuses returns all monitored core.EndpointStatus // GetAllEndpointStatuses returns all monitored endpoint.Status
// with a subset of core.Result defined by the page and pageSize parameters // with a subset of endpoint.Result defined by the page and pageSize parameters
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*core.EndpointStatus, error) { func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error) {
endpointStatuses := s.cache.GetAll() endpointStatuses := s.cache.GetAll()
pagedEndpointStatuses := make([]*core.EndpointStatus, 0, len(endpointStatuses)) pagedEndpointStatuses := make([]*endpoint.Status, 0, len(endpointStatuses))
for _, v := range endpointStatuses { for _, v := range endpointStatuses {
pagedEndpointStatuses = append(pagedEndpointStatuses, ShallowCopyEndpointStatus(v.(*core.EndpointStatus), params)) pagedEndpointStatuses = append(pagedEndpointStatuses, ShallowCopyEndpointStatus(v.(*endpoint.Status), params))
} }
sort.Slice(pagedEndpointStatuses, func(i, j int) bool { sort.Slice(pagedEndpointStatuses, func(i, j int) bool {
return pagedEndpointStatuses[i].Key < pagedEndpointStatuses[j].Key return pagedEndpointStatuses[i].Key < pagedEndpointStatuses[j].Key
@ -45,17 +44,17 @@ func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*
} }
// GetEndpointStatus returns the endpoint status for a given endpoint name in the given group // GetEndpointStatus returns the endpoint status for a given endpoint name in the given group
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) { func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
return s.GetEndpointStatusByKey(util.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params) return s.GetEndpointStatusByKey(endpoint.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
} }
// GetEndpointStatusByKey returns the endpoint status for a given key // GetEndpointStatusByKey returns the endpoint status for a given key
func (s *Store) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) { func (s *Store) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
endpointStatus := s.cache.GetValue(key) endpointStatus := s.cache.GetValue(key)
if endpointStatus == nil { if endpointStatus == nil {
return nil, common.ErrEndpointNotFound return nil, common.ErrEndpointNotFound
} }
return ShallowCopyEndpointStatus(endpointStatus.(*core.EndpointStatus), params), nil return ShallowCopyEndpointStatus(endpointStatus.(*endpoint.Status), params), nil
} }
// GetUptimeByKey returns the uptime percentage during a time range // GetUptimeByKey returns the uptime percentage during a time range
@ -64,7 +63,7 @@ func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error)
return 0, common.ErrInvalidTimeRange return 0, common.ErrInvalidTimeRange
} }
endpointStatus := s.cache.GetValue(key) endpointStatus := s.cache.GetValue(key)
if endpointStatus == nil || endpointStatus.(*core.EndpointStatus).Uptime == nil { if endpointStatus == nil || endpointStatus.(*endpoint.Status).Uptime == nil {
return 0, common.ErrEndpointNotFound return 0, common.ErrEndpointNotFound
} }
successfulExecutions := uint64(0) successfulExecutions := uint64(0)
@ -72,7 +71,7 @@ func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error)
current := from current := from
for to.Sub(current) >= 0 { for to.Sub(current) >= 0 {
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix() hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
hourlyStats := endpointStatus.(*core.EndpointStatus).Uptime.HourlyStatistics[hourlyUnixTimestamp] hourlyStats := endpointStatus.(*endpoint.Status).Uptime.HourlyStatistics[hourlyUnixTimestamp]
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 { if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
current = current.Add(time.Hour) current = current.Add(time.Hour)
continue continue
@ -93,14 +92,14 @@ func (s *Store) GetAverageResponseTimeByKey(key string, from, to time.Time) (int
return 0, common.ErrInvalidTimeRange return 0, common.ErrInvalidTimeRange
} }
endpointStatus := s.cache.GetValue(key) endpointStatus := s.cache.GetValue(key)
if endpointStatus == nil || endpointStatus.(*core.EndpointStatus).Uptime == nil { if endpointStatus == nil || endpointStatus.(*endpoint.Status).Uptime == nil {
return 0, common.ErrEndpointNotFound return 0, common.ErrEndpointNotFound
} }
current := from current := from
var totalExecutions, totalResponseTime uint64 var totalExecutions, totalResponseTime uint64
for to.Sub(current) >= 0 { for to.Sub(current) >= 0 {
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix() hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
hourlyStats := endpointStatus.(*core.EndpointStatus).Uptime.HourlyStatistics[hourlyUnixTimestamp] hourlyStats := endpointStatus.(*endpoint.Status).Uptime.HourlyStatistics[hourlyUnixTimestamp]
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 { if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
current = current.Add(time.Hour) current = current.Add(time.Hour)
continue continue
@ -121,14 +120,14 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time
return nil, common.ErrInvalidTimeRange return nil, common.ErrInvalidTimeRange
} }
endpointStatus := s.cache.GetValue(key) endpointStatus := s.cache.GetValue(key)
if endpointStatus == nil || endpointStatus.(*core.EndpointStatus).Uptime == nil { if endpointStatus == nil || endpointStatus.(*endpoint.Status).Uptime == nil {
return nil, common.ErrEndpointNotFound return nil, common.ErrEndpointNotFound
} }
hourlyAverageResponseTimes := make(map[int64]int) hourlyAverageResponseTimes := make(map[int64]int)
current := from current := from
for to.Sub(current) >= 0 { for to.Sub(current) >= 0 {
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix() hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
hourlyStats := endpointStatus.(*core.EndpointStatus).Uptime.HourlyStatistics[hourlyUnixTimestamp] hourlyStats := endpointStatus.(*endpoint.Status).Uptime.HourlyStatistics[hourlyUnixTimestamp]
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 { if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
current = current.Add(time.Hour) current = current.Add(time.Hour)
continue continue
@ -140,24 +139,24 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time
} }
// Insert adds the observed result for the specified endpoint into the store // Insert adds the observed result for the specified endpoint into the store
func (s *Store) Insert(endpoint *core.Endpoint, result *core.Result) error { func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error {
key := endpoint.Key() key := ep.Key()
s.Lock() s.Lock()
status, exists := s.cache.Get(key) status, exists := s.cache.Get(key)
if !exists { if !exists {
status = core.NewEndpointStatus(endpoint.Group, endpoint.Name) status = endpoint.NewStatus(ep.Group, ep.Name)
status.(*core.EndpointStatus).Events = append(status.(*core.EndpointStatus).Events, &core.Event{ status.(*endpoint.Status).Events = append(status.(*endpoint.Status).Events, &endpoint.Event{
Type: core.EventStart, Type: endpoint.EventStart,
Timestamp: time.Now(), Timestamp: time.Now(),
}) })
} }
AddResult(status.(*core.EndpointStatus), result) AddResult(status.(*endpoint.Status), result)
s.cache.Set(key, status) s.cache.Set(key, status)
s.Unlock() s.Unlock()
return nil return nil
} }
// DeleteAllEndpointStatusesNotInKeys removes all EndpointStatus that are not within the keys provided // DeleteAllEndpointStatusesNotInKeys removes all Status that are not within the keys provided
func (s *Store) DeleteAllEndpointStatusesNotInKeys(keys []string) int { func (s *Store) DeleteAllEndpointStatusesNotInKeys(keys []string) int {
var keysToDelete []string var keysToDelete []string
for _, existingKey := range s.cache.GetKeysByPattern("*", 0) { for _, existingKey := range s.cache.GetKeysByPattern("*", 0) {

View file

@ -4,30 +4,30 @@ import (
"testing" "testing"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
) )
var ( var (
firstCondition = core.Condition("[STATUS] == 200") firstCondition = endpoint.Condition("[STATUS] == 200")
secondCondition = core.Condition("[RESPONSE_TIME] < 500") secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h") thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
now = time.Now() now = time.Now()
testEndpoint = core.Endpoint{ testEndpoint = endpoint.Endpoint{
Name: "name", Name: "name",
Group: "group", Group: "group",
URL: "https://example.org/what/ever", URL: "https://example.org/what/ever",
Method: "GET", Method: "GET",
Body: "body", Body: "body",
Interval: 30 * time.Second, Interval: 30 * time.Second,
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition}, Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
Alerts: nil, Alerts: nil,
NumberOfFailuresInARow: 0, NumberOfFailuresInARow: 0,
NumberOfSuccessesInARow: 0, NumberOfSuccessesInARow: 0,
} }
testSuccessfulResult = core.Result{ testSuccessfulResult = endpoint.Result{
Hostname: "example.org", Hostname: "example.org",
IP: "127.0.0.1", IP: "127.0.0.1",
HTTPStatus: 200, HTTPStatus: 200,
@ -37,7 +37,7 @@ var (
Timestamp: now, Timestamp: now,
Duration: 150 * time.Millisecond, Duration: 150 * time.Millisecond,
CertificateExpiration: 10 * time.Hour, CertificateExpiration: 10 * time.Hour,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: true, Success: true,
@ -52,7 +52,7 @@ var (
}, },
}, },
} }
testUnsuccessfulResult = core.Result{ testUnsuccessfulResult = endpoint.Result{
Hostname: "example.org", Hostname: "example.org",
IP: "127.0.0.1", IP: "127.0.0.1",
HTTPStatus: 200, HTTPStatus: 200,
@ -62,7 +62,7 @@ var (
Timestamp: now, Timestamp: now,
Duration: 750 * time.Millisecond, Duration: 750 * time.Millisecond,
CertificateExpiration: 10 * time.Hour, CertificateExpiration: 10 * time.Hour,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: true, Success: true,

View file

@ -3,7 +3,7 @@ package memory
import ( import (
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
const ( const (
@ -13,14 +13,14 @@ const (
// processUptimeAfterResult processes the result by extracting the relevant from the result and recalculating the uptime // processUptimeAfterResult processes the result by extracting the relevant from the result and recalculating the uptime
// if necessary // if necessary
func processUptimeAfterResult(uptime *core.Uptime, result *core.Result) { func processUptimeAfterResult(uptime *endpoint.Uptime, result *endpoint.Result) {
if uptime.HourlyStatistics == nil { if uptime.HourlyStatistics == nil {
uptime.HourlyStatistics = make(map[int64]*core.HourlyUptimeStatistics) uptime.HourlyStatistics = make(map[int64]*endpoint.HourlyUptimeStatistics)
} }
unixTimestampFlooredAtHour := result.Timestamp.Truncate(time.Hour).Unix() unixTimestampFlooredAtHour := result.Timestamp.Truncate(time.Hour).Unix()
hourlyStats, _ := uptime.HourlyStatistics[unixTimestampFlooredAtHour] hourlyStats, _ := uptime.HourlyStatistics[unixTimestampFlooredAtHour]
if hourlyStats == nil { if hourlyStats == nil {
hourlyStats = &core.HourlyUptimeStatistics{} hourlyStats = &endpoint.HourlyUptimeStatistics{}
uptime.HourlyStatistics[unixTimestampFlooredAtHour] = hourlyStats uptime.HourlyStatistics[unixTimestampFlooredAtHour] = hourlyStats
} }
if result.Success { if result.Success {

View file

@ -4,17 +4,17 @@ import (
"testing" "testing"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
func BenchmarkProcessUptimeAfterResult(b *testing.B) { func BenchmarkProcessUptimeAfterResult(b *testing.B) {
uptime := core.NewUptime() uptime := endpoint.NewUptime()
now := time.Now() now := time.Now()
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
// Start 12000 days ago // Start 12000 days ago
timestamp := now.Add(-12000 * 24 * time.Hour) timestamp := now.Add(-12000 * 24 * time.Hour)
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
processUptimeAfterResult(uptime, &core.Result{ processUptimeAfterResult(uptime, &endpoint.Result{
Duration: 18 * time.Millisecond, Duration: 18 * time.Millisecond,
Success: n%15 == 0, Success: n%15 == 0,
Timestamp: timestamp, Timestamp: timestamp,

View file

@ -4,53 +4,53 @@ import (
"testing" "testing"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
) )
func TestProcessUptimeAfterResult(t *testing.T) { func TestProcessUptimeAfterResult(t *testing.T) {
endpoint := &core.Endpoint{Name: "name", Group: "group"} ep := &endpoint.Endpoint{Name: "name", Group: "group"}
status := core.NewEndpointStatus(endpoint.Group, endpoint.Name) status := endpoint.NewStatus(ep.Group, ep.Name)
uptime := status.Uptime uptime := status.Uptime
now := time.Now() now := time.Now()
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-7 * 24 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-7 * 24 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-6 * 24 * time.Hour), Success: false}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-6 * 24 * time.Hour), Success: false})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-8 * 24 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-8 * 24 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-24 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-24 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-12 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-12 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-1 * time.Hour), Success: true, Duration: 10 * time.Millisecond}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-1 * time.Hour), Success: true, Duration: 10 * time.Millisecond})
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 10, 1, 1) checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 10, 1, 1)
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-30 * time.Minute), Success: false, Duration: 500 * time.Millisecond}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-30 * time.Minute), Success: false, Duration: 500 * time.Millisecond})
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 510, 2, 1) checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 510, 2, 1)
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-15 * time.Minute), Success: false, Duration: 25 * time.Millisecond}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-15 * time.Minute), Success: false, Duration: 25 * time.Millisecond})
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 535, 3, 1) checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 535, 3, 1)
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-10 * time.Minute), Success: false}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-10 * time.Minute), Success: false})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-120 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-120 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-119 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-119 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-118 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-118 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-117 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-117 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-10 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-10 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-8 * time.Hour), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-30 * time.Minute), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-30 * time.Minute), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-25 * time.Minute), Success: true}) processUptimeAfterResult(uptime, &endpoint.Result{Timestamp: now.Add(-25 * time.Minute), Success: true})
} }
func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) { func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
endpoint := &core.Endpoint{Name: "name", Group: "group"} ep := &endpoint.Endpoint{Name: "name", Group: "group"}
status := core.NewEndpointStatus(endpoint.Group, endpoint.Name) status := endpoint.NewStatus(ep.Group, ep.Name)
now := time.Now() now := time.Now()
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
// Start 12 days ago // Start 12 days ago
timestamp := now.Add(-12 * 24 * time.Hour) timestamp := now.Add(-12 * 24 * time.Hour)
for timestamp.Unix() <= now.Unix() { for timestamp.Unix() <= now.Unix() {
AddResult(status, &core.Result{Timestamp: timestamp, Success: true}) AddResult(status, &endpoint.Result{Timestamp: timestamp, Success: true})
if len(status.Uptime.HourlyStatistics) > numberOfHoursInTenDays { if len(status.Uptime.HourlyStatistics) > numberOfHoursInTenDays {
t.Errorf("At no point in time should there be more than %d entries in status.SuccessfulExecutionsPerHour, but there are %d", numberOfHoursInTenDays, len(status.Uptime.HourlyStatistics)) t.Errorf("At no point in time should there be more than %d entries in status.SuccessfulExecutionsPerHour, but there are %d", numberOfHoursInTenDays, len(status.Uptime.HourlyStatistics))
} }
@ -59,7 +59,7 @@ func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
} }
} }
func checkHourlyStatistics(t *testing.T, hourlyUptimeStatistics *core.HourlyUptimeStatistics, expectedTotalExecutionsResponseTime uint64, expectedTotalExecutions uint64, expectedSuccessfulExecutions uint64) { func checkHourlyStatistics(t *testing.T, hourlyUptimeStatistics *endpoint.HourlyUptimeStatistics, expectedTotalExecutionsResponseTime uint64, expectedTotalExecutions uint64, expectedSuccessfulExecutions uint64) {
if hourlyUptimeStatistics.TotalExecutionsResponseTime != expectedTotalExecutionsResponseTime { if hourlyUptimeStatistics.TotalExecutionsResponseTime != expectedTotalExecutionsResponseTime {
t.Error("TotalExecutionsResponseTime should've been", expectedTotalExecutionsResponseTime, "got", hourlyUptimeStatistics.TotalExecutionsResponseTime) t.Error("TotalExecutionsResponseTime should've been", expectedTotalExecutionsResponseTime, "got", hourlyUptimeStatistics.TotalExecutionsResponseTime)
} }

View file

@ -1,31 +1,31 @@
package memory package memory
import ( import (
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
) )
// ShallowCopyEndpointStatus returns a shallow copy of a EndpointStatus with only the results // ShallowCopyEndpointStatus returns a shallow copy of a Status with only the results
// within the range defined by the page and pageSize parameters // within the range defined by the page and pageSize parameters
func ShallowCopyEndpointStatus(ss *core.EndpointStatus, params *paging.EndpointStatusParams) *core.EndpointStatus { func ShallowCopyEndpointStatus(ss *endpoint.Status, params *paging.EndpointStatusParams) *endpoint.Status {
shallowCopy := &core.EndpointStatus{ shallowCopy := &endpoint.Status{
Name: ss.Name, Name: ss.Name,
Group: ss.Group, Group: ss.Group,
Key: ss.Key, Key: ss.Key,
Uptime: core.NewUptime(), Uptime: endpoint.NewUptime(),
} }
numberOfResults := len(ss.Results) numberOfResults := len(ss.Results)
resultsStart, resultsEnd := getStartAndEndIndex(numberOfResults, params.ResultsPage, params.ResultsPageSize) resultsStart, resultsEnd := getStartAndEndIndex(numberOfResults, params.ResultsPage, params.ResultsPageSize)
if resultsStart < 0 || resultsEnd < 0 { if resultsStart < 0 || resultsEnd < 0 {
shallowCopy.Results = []*core.Result{} shallowCopy.Results = []*endpoint.Result{}
} else { } else {
shallowCopy.Results = ss.Results[resultsStart:resultsEnd] shallowCopy.Results = ss.Results[resultsStart:resultsEnd]
} }
numberOfEvents := len(ss.Events) numberOfEvents := len(ss.Events)
eventsStart, eventsEnd := getStartAndEndIndex(numberOfEvents, params.EventsPage, params.EventsPageSize) eventsStart, eventsEnd := getStartAndEndIndex(numberOfEvents, params.EventsPage, params.EventsPageSize)
if eventsStart < 0 || eventsEnd < 0 { if eventsStart < 0 || eventsEnd < 0 {
shallowCopy.Events = []*core.Event{} shallowCopy.Events = []*endpoint.Event{}
} else { } else {
shallowCopy.Events = ss.Events[eventsStart:eventsEnd] shallowCopy.Events = ss.Events[eventsStart:eventsEnd]
} }
@ -49,16 +49,16 @@ func getStartAndEndIndex(numberOfResults int, page, pageSize int) (int, int) {
return start, end return start, end
} }
// AddResult adds a Result to EndpointStatus.Results and makes sure that there are // AddResult adds a Result to Status.Results and makes sure that there are
// no more than MaximumNumberOfResults results in the Results slice // no more than MaximumNumberOfResults results in the Results slice
func AddResult(ss *core.EndpointStatus, result *core.Result) { func AddResult(ss *endpoint.Status, result *endpoint.Result) {
if ss == nil { if ss == nil {
return return
} }
if len(ss.Results) > 0 { if len(ss.Results) > 0 {
// Check if there's any change since the last result // Check if there's any change since the last result
if ss.Results[len(ss.Results)-1].Success != result.Success { if ss.Results[len(ss.Results)-1].Success != result.Success {
ss.Events = append(ss.Events, core.NewEventFromResult(result)) ss.Events = append(ss.Events, endpoint.NewEventFromResult(result))
if len(ss.Events) > common.MaximumNumberOfEvents { if len(ss.Events) > common.MaximumNumberOfEvents {
// Doing ss.Events[1:] would usually be sufficient, but in the case where for some reason, the slice has // Doing ss.Events[1:] would usually be sufficient, but in the case where for some reason, the slice has
// more than one extra element, we can get rid of all of them at once and thus returning the slice to a // more than one extra element, we can get rid of all of them at once and thus returning the slice to a
@ -68,7 +68,7 @@ func AddResult(ss *core.EndpointStatus, result *core.Result) {
} }
} else { } else {
// This is the first result, so we need to add the first healthy/unhealthy event // This is the first result, so we need to add the first healthy/unhealthy event
ss.Events = append(ss.Events, core.NewEventFromResult(result)) ss.Events = append(ss.Events, endpoint.NewEventFromResult(result))
} }
ss.Results = append(ss.Results, result) ss.Results = append(ss.Results, result)
if len(ss.Results) > common.MaximumNumberOfResults { if len(ss.Results) > common.MaximumNumberOfResults {

View file

@ -3,14 +3,14 @@ package memory
import ( import (
"testing" "testing"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
) )
func BenchmarkShallowCopyEndpointStatus(b *testing.B) { func BenchmarkShallowCopyEndpointStatus(b *testing.B) {
endpoint := &testEndpoint ep := &testEndpoint
status := core.NewEndpointStatus(endpoint.Group, endpoint.Name) status := endpoint.NewStatus(ep.Group, ep.Name)
for i := 0; i < common.MaximumNumberOfResults; i++ { for i := 0; i < common.MaximumNumberOfResults; i++ {
AddResult(status, &testSuccessfulResult) AddResult(status, &testSuccessfulResult)
} }

View file

@ -4,16 +4,16 @@ import (
"testing" "testing"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
) )
func TestAddResult(t *testing.T) { func TestAddResult(t *testing.T) {
endpoint := &core.Endpoint{Name: "name", Group: "group"} ep := &endpoint.Endpoint{Name: "name", Group: "group"}
endpointStatus := core.NewEndpointStatus(endpoint.Group, endpoint.Name) endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
for i := 0; i < (common.MaximumNumberOfResults+common.MaximumNumberOfEvents)*2; i++ { for i := 0; i < (common.MaximumNumberOfResults+common.MaximumNumberOfEvents)*2; i++ {
AddResult(endpointStatus, &core.Result{Success: i%2 == 0, Timestamp: time.Now()}) AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: time.Now()})
} }
if len(endpointStatus.Results) != common.MaximumNumberOfResults { if len(endpointStatus.Results) != common.MaximumNumberOfResults {
t.Errorf("expected endpointStatus.Results to not exceed a length of %d", common.MaximumNumberOfResults) t.Errorf("expected endpointStatus.Results to not exceed a length of %d", common.MaximumNumberOfResults)
@ -22,15 +22,15 @@ func TestAddResult(t *testing.T) {
t.Errorf("expected endpointStatus.Events to not exceed a length of %d", common.MaximumNumberOfEvents) t.Errorf("expected endpointStatus.Events to not exceed a length of %d", common.MaximumNumberOfEvents)
} }
// Try to add nil endpointStatus // Try to add nil endpointStatus
AddResult(nil, &core.Result{Timestamp: time.Now()}) AddResult(nil, &endpoint.Result{Timestamp: time.Now()})
} }
func TestShallowCopyEndpointStatus(t *testing.T) { func TestShallowCopyEndpointStatus(t *testing.T) {
endpoint := &core.Endpoint{Name: "name", Group: "group"} ep := &endpoint.Endpoint{Name: "name", Group: "group"}
endpointStatus := core.NewEndpointStatus(endpoint.Group, endpoint.Name) endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
ts := time.Now().Add(-25 * time.Hour) ts := time.Now().Add(-25 * time.Hour)
for i := 0; i < 25; i++ { for i := 0; i < 25; i++ {
AddResult(endpointStatus, &core.Result{Success: i%2 == 0, Timestamp: ts}) AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: ts})
ts = ts.Add(time.Hour) ts = ts.Add(time.Hour)
} }
if len(ShallowCopyEndpointStatus(endpointStatus, paging.NewEndpointStatusParams().WithResults(-1, -1)).Results) != 0 { if len(ShallowCopyEndpointStatus(endpointStatus, paging.NewEndpointStatusParams().WithResults(-1, -1)).Results) != 0 {

View file

@ -9,10 +9,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
"github.com/TwiN/gatus/v5/util"
"github.com/TwiN/gocache/v2" "github.com/TwiN/gocache/v2"
_ "github.com/lib/pq" _ "github.com/lib/pq"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
@ -100,9 +99,9 @@ func (s *Store) createSchema() error {
return s.createPostgresSchema() return s.createPostgresSchema()
} }
// GetAllEndpointStatuses returns all monitored core.EndpointStatus // GetAllEndpointStatuses returns all monitored endpoint.Status
// with a subset of core.Result defined by the page and pageSize parameters // with a subset of endpoint.Result defined by the page and pageSize parameters
func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*core.EndpointStatus, error) { func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error) {
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
return nil, err return nil, err
@ -112,7 +111,7 @@ func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*
_ = tx.Rollback() _ = tx.Rollback()
return nil, err return nil, err
} }
endpointStatuses := make([]*core.EndpointStatus, 0, len(keys)) endpointStatuses := make([]*endpoint.Status, 0, len(keys))
for _, key := range keys { for _, key := range keys {
endpointStatus, err := s.getEndpointStatusByKey(tx, key, params) endpointStatus, err := s.getEndpointStatusByKey(tx, key, params)
if err != nil { if err != nil {
@ -127,12 +126,12 @@ func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*
} }
// GetEndpointStatus returns the endpoint status for a given endpoint name in the given group // GetEndpointStatus returns the endpoint status for a given endpoint name in the given group
func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) { func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
return s.GetEndpointStatusByKey(util.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params) return s.GetEndpointStatusByKey(endpoint.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params)
} }
// GetEndpointStatusByKey returns the endpoint status for a given key // GetEndpointStatusByKey returns the endpoint status for a given key
func (s *Store) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) { func (s *Store) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*endpoint.Status, error) {
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
return nil, err return nil, err
@ -224,30 +223,30 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time
} }
// Insert adds the observed result for the specified endpoint into the store // Insert adds the observed result for the specified endpoint into the store
func (s *Store) Insert(endpoint *core.Endpoint, result *core.Result) error { func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error {
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
return err return err
} }
endpointID, err := s.getEndpointID(tx, endpoint) endpointID, err := s.getEndpointID(tx, ep)
if err != nil { if err != nil {
if errors.Is(err, common.ErrEndpointNotFound) { if errors.Is(err, common.ErrEndpointNotFound) {
// Endpoint doesn't exist in the database, insert it // Endpoint doesn't exist in the database, insert it
if endpointID, err = s.insertEndpoint(tx, endpoint); err != nil { if endpointID, err = s.insertEndpoint(tx, ep); err != nil {
_ = tx.Rollback() _ = tx.Rollback()
log.Printf("[sql.Insert] Failed to create endpoint with group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to create endpoint with group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
return err return err
} }
} else { } else {
_ = tx.Rollback() _ = tx.Rollback()
log.Printf("[sql.Insert] Failed to retrieve id of endpoint with group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to retrieve id of endpoint with group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
return err return err
} }
} }
// First, we need to check if we need to insert a new event. // First, we need to check if we need to insert a new event.
// //
// A new event must be added if either of the following cases happen: // A new event must be added if either of the following cases happen:
// 1. There is only 1 event. The total number of events for a endpoint can only be 1 if the only existing event is // 1. There is only 1 event. The total number of events for an endpoint can only be 1 if the only existing event is
// of type EventStart, in which case we will have to create a new event of type EventHealthy or EventUnhealthy // of type EventStart, in which case we will have to create a new event of type EventHealthy or EventUnhealthy
// based on result.Success. // based on result.Success.
// 2. The lastResult.Success != result.Success. This implies that the endpoint went from healthy to unhealthy or // 2. The lastResult.Success != result.Success. This implies that the endpoint went from healthy to unhealthy or
@ -256,38 +255,38 @@ func (s *Store) Insert(endpoint *core.Endpoint, result *core.Result) error {
numberOfEvents, err := s.getNumberOfEventsByEndpointID(tx, endpointID) numberOfEvents, err := s.getNumberOfEventsByEndpointID(tx, endpointID)
if err != nil { if err != nil {
// Silently fail // Silently fail
log.Printf("[sql.Insert] Failed to retrieve total number of events for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to retrieve total number of events for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
} }
if numberOfEvents == 0 { if numberOfEvents == 0 {
// There's no events yet, which means we need to add the EventStart and the first healthy/unhealthy event // There's no events yet, which means we need to add the EventStart and the first healthy/unhealthy event
err = s.insertEndpointEvent(tx, endpointID, &core.Event{ err = s.insertEndpointEvent(tx, endpointID, &endpoint.Event{
Type: core.EventStart, Type: endpoint.EventStart,
Timestamp: result.Timestamp.Add(-50 * time.Millisecond), Timestamp: result.Timestamp.Add(-50 * time.Millisecond),
}) })
if err != nil { if err != nil {
// Silently fail // Silently fail
log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", core.EventStart, endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", endpoint.EventStart, ep.Group, ep.Name, err.Error())
} }
event := core.NewEventFromResult(result) event := endpoint.NewEventFromResult(result)
if err = s.insertEndpointEvent(tx, endpointID, event); err != nil { if err = s.insertEndpointEvent(tx, endpointID, event); err != nil {
// Silently fail // Silently fail
log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", event.Type, endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", event.Type, ep.Group, ep.Name, err.Error())
} }
} else { } else {
// Get the success value of the previous result // Get the success value of the previous result
var lastResultSuccess bool var lastResultSuccess bool
if lastResultSuccess, err = s.getLastEndpointResultSuccessValue(tx, endpointID); err != nil { if lastResultSuccess, err = s.getLastEndpointResultSuccessValue(tx, endpointID); err != nil {
log.Printf("[sql.Insert] Failed to retrieve outcome of previous result for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to retrieve outcome of previous result for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
} else { } else {
// If we managed to retrieve the outcome of the previous result, we'll compare it with the new result. // If we managed to retrieve the outcome of the previous result, we'll compare it with the new result.
// If the final outcome (success or failure) of the previous and the new result aren't the same, it means // If the final outcome (success or failure) of the previous and the new result aren't the same, it means
// that the endpoint either went from Healthy to Unhealthy or Unhealthy -> Healthy, therefore, we'll add // that the endpoint either went from Healthy to Unhealthy or Unhealthy -> Healthy, therefore, we'll add
// an event to mark the change in state // an event to mark the change in state
if lastResultSuccess != result.Success { if lastResultSuccess != result.Success {
event := core.NewEventFromResult(result) event := endpoint.NewEventFromResult(result)
if err = s.insertEndpointEvent(tx, endpointID, event); err != nil { if err = s.insertEndpointEvent(tx, endpointID, event); err != nil {
// Silently fail // Silently fail
log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", event.Type, endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to insert event=%s for group=%s; endpoint=%s: %s", event.Type, ep.Group, ep.Name, err.Error())
} }
} }
} }
@ -296,45 +295,45 @@ func (s *Store) Insert(endpoint *core.Endpoint, result *core.Result) error {
// (since we're only deleting MaximumNumberOfEvents at a time instead of 1) // (since we're only deleting MaximumNumberOfEvents at a time instead of 1)
if numberOfEvents > eventsCleanUpThreshold { if numberOfEvents > eventsCleanUpThreshold {
if err = s.deleteOldEndpointEvents(tx, endpointID); err != nil { if err = s.deleteOldEndpointEvents(tx, endpointID); err != nil {
log.Printf("[sql.Insert] Failed to delete old events for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to delete old events for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
} }
} }
} }
// Second, we need to insert the result. // Second, we need to insert the result.
if err = s.insertEndpointResult(tx, endpointID, result); err != nil { if err = s.insertEndpointResult(tx, endpointID, result); err != nil {
log.Printf("[sql.Insert] Failed to insert result for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to insert result for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
_ = tx.Rollback() // If we can't insert the result, we'll rollback now since there's no point continuing _ = tx.Rollback() // If we can't insert the result, we'll rollback now since there's no point continuing
return err return err
} }
// Clean up old results // Clean up old results
numberOfResults, err := s.getNumberOfResultsByEndpointID(tx, endpointID) numberOfResults, err := s.getNumberOfResultsByEndpointID(tx, endpointID)
if err != nil { if err != nil {
log.Printf("[sql.Insert] Failed to retrieve total number of results for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to retrieve total number of results for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
} else { } else {
if numberOfResults > resultsCleanUpThreshold { if numberOfResults > resultsCleanUpThreshold {
if err = s.deleteOldEndpointResults(tx, endpointID); err != nil { if err = s.deleteOldEndpointResults(tx, endpointID); err != nil {
log.Printf("[sql.Insert] Failed to delete old results for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to delete old results for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
} }
} }
} }
// Finally, we need to insert the uptime data. // Finally, we need to insert the uptime data.
// Because the uptime data significantly outlives the results, we can't rely on the results for determining the uptime // Because the uptime data significantly outlives the results, we can't rely on the results for determining the uptime
if err = s.updateEndpointUptime(tx, endpointID, result); err != nil { if err = s.updateEndpointUptime(tx, endpointID, result); err != nil {
log.Printf("[sql.Insert] Failed to update uptime for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to update uptime for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
} }
// Clean up old uptime entries // Clean up old uptime entries
ageOfOldestUptimeEntry, err := s.getAgeOfOldestEndpointUptimeEntry(tx, endpointID) ageOfOldestUptimeEntry, err := s.getAgeOfOldestEndpointUptimeEntry(tx, endpointID)
if err != nil { if err != nil {
log.Printf("[sql.Insert] Failed to retrieve oldest endpoint uptime entry for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to retrieve oldest endpoint uptime entry for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
} else { } else {
if ageOfOldestUptimeEntry > uptimeCleanUpThreshold { if ageOfOldestUptimeEntry > uptimeCleanUpThreshold {
if err = s.deleteOldUptimeEntries(tx, endpointID, time.Now().Add(-(uptimeRetention + time.Hour))); err != nil { if err = s.deleteOldUptimeEntries(tx, endpointID, time.Now().Add(-(uptimeRetention + time.Hour))); err != nil {
log.Printf("[sql.Insert] Failed to delete old uptime entries for group=%s; endpoint=%s: %s", endpoint.Group, endpoint.Name, err.Error()) log.Printf("[sql.Insert] Failed to delete old uptime entries for group=%s; endpoint=%s: %s", ep.Group, ep.Name, err.Error())
} }
} }
} }
if s.writeThroughCache != nil { if s.writeThroughCache != nil {
cacheKeysToRefresh := s.writeThroughCache.GetKeysByPattern(endpoint.Key()+"*", 0) cacheKeysToRefresh := s.writeThroughCache.GetKeysByPattern(ep.Key()+"*", 0)
for _, cacheKey := range cacheKeysToRefresh { for _, cacheKey := range cacheKeysToRefresh {
s.writeThroughCache.Delete(cacheKey) s.writeThroughCache.Delete(cacheKey)
endpointKey, params, err := extractKeyAndParamsFromCacheKey(cacheKey) endpointKey, params, err := extractKeyAndParamsFromCacheKey(cacheKey)
@ -405,14 +404,14 @@ func (s *Store) Close() {
} }
// insertEndpoint inserts an endpoint in the store and returns the generated id of said endpoint // insertEndpoint inserts an endpoint in the store and returns the generated id of said endpoint
func (s *Store) insertEndpoint(tx *sql.Tx, endpoint *core.Endpoint) (int64, error) { func (s *Store) insertEndpoint(tx *sql.Tx, ep *endpoint.Endpoint) (int64, error) {
//log.Printf("[sql.insertEndpoint] Inserting endpoint with group=%s and name=%s", endpoint.Group, endpoint.Name) //log.Printf("[sql.insertEndpoint] Inserting endpoint with group=%s and name=%s", ep.Group, ep.Name)
var id int64 var id int64
err := tx.QueryRow( err := tx.QueryRow(
"INSERT INTO endpoints (endpoint_key, endpoint_name, endpoint_group) VALUES ($1, $2, $3) RETURNING endpoint_id", "INSERT INTO endpoints (endpoint_key, endpoint_name, endpoint_group) VALUES ($1, $2, $3) RETURNING endpoint_id",
endpoint.Key(), ep.Key(),
endpoint.Name, ep.Name,
endpoint.Group, ep.Group,
).Scan(&id) ).Scan(&id)
if err != nil { if err != nil {
return 0, err return 0, err
@ -421,7 +420,7 @@ func (s *Store) insertEndpoint(tx *sql.Tx, endpoint *core.Endpoint) (int64, erro
} }
// insertEndpointEvent inserts en event in the store // insertEndpointEvent inserts en event in the store
func (s *Store) insertEndpointEvent(tx *sql.Tx, endpointID int64, event *core.Event) error { func (s *Store) insertEndpointEvent(tx *sql.Tx, endpointID int64, event *endpoint.Event) error {
_, err := tx.Exec( _, err := tx.Exec(
"INSERT INTO endpoint_events (endpoint_id, event_type, event_timestamp) VALUES ($1, $2, $3)", "INSERT INTO endpoint_events (endpoint_id, event_type, event_timestamp) VALUES ($1, $2, $3)",
endpointID, endpointID,
@ -435,7 +434,7 @@ func (s *Store) insertEndpointEvent(tx *sql.Tx, endpointID int64, event *core.Ev
} }
// insertEndpointResult inserts a result in the store // insertEndpointResult inserts a result in the store
func (s *Store) insertEndpointResult(tx *sql.Tx, endpointID int64, result *core.Result) error { func (s *Store) insertEndpointResult(tx *sql.Tx, endpointID int64, result *endpoint.Result) error {
var endpointResultID int64 var endpointResultID int64
err := tx.QueryRow( err := tx.QueryRow(
` `
@ -462,7 +461,7 @@ func (s *Store) insertEndpointResult(tx *sql.Tx, endpointID int64, result *core.
return s.insertConditionResults(tx, endpointResultID, result.ConditionResults) return s.insertConditionResults(tx, endpointResultID, result.ConditionResults)
} }
func (s *Store) insertConditionResults(tx *sql.Tx, endpointResultID int64, conditionResults []*core.ConditionResult) error { func (s *Store) insertConditionResults(tx *sql.Tx, endpointResultID int64, conditionResults []*endpoint.ConditionResult) error {
var err error var err error
for _, cr := range conditionResults { for _, cr := range conditionResults {
_, err = tx.Exec("INSERT INTO endpoint_result_conditions (endpoint_result_id, condition, success) VALUES ($1, $2, $3)", _, err = tx.Exec("INSERT INTO endpoint_result_conditions (endpoint_result_id, condition, success) VALUES ($1, $2, $3)",
@ -477,7 +476,7 @@ func (s *Store) insertConditionResults(tx *sql.Tx, endpointResultID int64, condi
return nil return nil
} }
func (s *Store) updateEndpointUptime(tx *sql.Tx, endpointID int64, result *core.Result) error { func (s *Store) updateEndpointUptime(tx *sql.Tx, endpointID int64, result *endpoint.Result) error {
unixTimestampFlooredAtHour := result.Timestamp.Truncate(time.Hour).Unix() unixTimestampFlooredAtHour := result.Timestamp.Truncate(time.Hour).Unix()
var successfulExecutions int var successfulExecutions int
if result.Success { if result.Success {
@ -514,12 +513,12 @@ func (s *Store) getAllEndpointKeys(tx *sql.Tx) (keys []string, err error) {
return return
} }
func (s *Store) getEndpointStatusByKey(tx *sql.Tx, key string, parameters *paging.EndpointStatusParams) (*core.EndpointStatus, error) { func (s *Store) getEndpointStatusByKey(tx *sql.Tx, key string, parameters *paging.EndpointStatusParams) (*endpoint.Status, error) {
var cacheKey string var cacheKey string
if s.writeThroughCache != nil { if s.writeThroughCache != nil {
cacheKey = generateCacheKey(key, parameters) cacheKey = generateCacheKey(key, parameters)
if cachedEndpointStatus, exists := s.writeThroughCache.Get(cacheKey); exists { if cachedEndpointStatus, exists := s.writeThroughCache.Get(cacheKey); exists {
if castedCachedEndpointStatus, ok := cachedEndpointStatus.(*core.EndpointStatus); ok { if castedCachedEndpointStatus, ok := cachedEndpointStatus.(*endpoint.Status); ok {
return castedCachedEndpointStatus, nil return castedCachedEndpointStatus, nil
} }
} }
@ -528,7 +527,7 @@ func (s *Store) getEndpointStatusByKey(tx *sql.Tx, key string, parameters *pagin
if err != nil { if err != nil {
return nil, err return nil, err
} }
endpointStatus := core.NewEndpointStatus(group, endpointName) endpointStatus := endpoint.NewStatus(group, endpointName)
if parameters.EventsPageSize > 0 { if parameters.EventsPageSize > 0 {
if endpointStatus.Events, err = s.getEndpointEventsByEndpointID(tx, endpointID, parameters.EventsPage, parameters.EventsPageSize); err != nil { if endpointStatus.Events, err = s.getEndpointEventsByEndpointID(tx, endpointID, parameters.EventsPage, parameters.EventsPageSize); err != nil {
log.Printf("[sql.getEndpointStatusByKey] Failed to retrieve events for key=%s: %s", key, err.Error()) log.Printf("[sql.getEndpointStatusByKey] Failed to retrieve events for key=%s: %s", key, err.Error())
@ -564,7 +563,7 @@ func (s *Store) getEndpointIDGroupAndNameByKey(tx *sql.Tx, key string) (id int64
return return
} }
func (s *Store) getEndpointEventsByEndpointID(tx *sql.Tx, endpointID int64, page, pageSize int) (events []*core.Event, err error) { func (s *Store) getEndpointEventsByEndpointID(tx *sql.Tx, endpointID int64, page, pageSize int) (events []*endpoint.Event, err error) {
rows, err := tx.Query( rows, err := tx.Query(
` `
SELECT event_type, event_timestamp SELECT event_type, event_timestamp
@ -581,14 +580,14 @@ func (s *Store) getEndpointEventsByEndpointID(tx *sql.Tx, endpointID int64, page
return nil, err return nil, err
} }
for rows.Next() { for rows.Next() {
event := &core.Event{} event := &endpoint.Event{}
_ = rows.Scan(&event.Type, &event.Timestamp) _ = rows.Scan(&event.Type, &event.Timestamp)
events = append(events, event) events = append(events, event)
} }
return return
} }
func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, page, pageSize int) (results []*core.Result, err error) { func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, page, pageSize int) (results []*endpoint.Result, err error) {
rows, err := tx.Query( rows, err := tx.Query(
` `
SELECT endpoint_result_id, success, errors, connected, status, dns_rcode, certificate_expiration, domain_expiration, hostname, ip, duration, timestamp SELECT endpoint_result_id, success, errors, connected, status, dns_rcode, certificate_expiration, domain_expiration, hostname, ip, duration, timestamp
@ -604,9 +603,9 @@ func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, pag
if err != nil { if err != nil {
return nil, err return nil, err
} }
idResultMap := make(map[int64]*core.Result) idResultMap := make(map[int64]*endpoint.Result)
for rows.Next() { for rows.Next() {
result := &core.Result{} result := &endpoint.Result{}
var id int64 var id int64
var joinedErrors string var joinedErrors string
err = rows.Scan(&id, &result.Success, &joinedErrors, &result.Connected, &result.HTTPStatus, &result.DNSRCode, &result.CertificateExpiration, &result.DomainExpiration, &result.Hostname, &result.IP, &result.Duration, &result.Timestamp) err = rows.Scan(&id, &result.Success, &joinedErrors, &result.Connected, &result.HTTPStatus, &result.DNSRCode, &result.CertificateExpiration, &result.DomainExpiration, &result.Hostname, &result.IP, &result.Duration, &result.Timestamp)
@ -618,7 +617,7 @@ func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, pag
result.Errors = strings.Split(joinedErrors, arraySeparator) result.Errors = strings.Split(joinedErrors, arraySeparator)
} }
// This is faster than using a subselect // This is faster than using a subselect
results = append([]*core.Result{result}, results...) results = append([]*endpoint.Result{result}, results...)
idResultMap[id] = result idResultMap[id] = result
} }
if len(idResultMap) == 0 { if len(idResultMap) == 0 {
@ -643,7 +642,7 @@ func (s *Store) getEndpointResultsByEndpointID(tx *sql.Tx, endpointID int64, pag
} }
defer rows.Close() // explicitly defer the close in case an error happens during the scan defer rows.Close() // explicitly defer the close in case an error happens during the scan
for rows.Next() { for rows.Next() {
conditionResult := &core.ConditionResult{} conditionResult := &endpoint.ConditionResult{}
var endpointResultID int64 var endpointResultID int64
if err = rows.Scan(&endpointResultID, &conditionResult.Condition, &conditionResult.Success); err != nil { if err = rows.Scan(&endpointResultID, &conditionResult.Condition, &conditionResult.Success); err != nil {
return return
@ -734,9 +733,9 @@ func (s *Store) getEndpointHourlyAverageResponseTimes(tx *sql.Tx, endpointID int
return hourlyAverageResponseTimes, nil return hourlyAverageResponseTimes, nil
} }
func (s *Store) getEndpointID(tx *sql.Tx, endpoint *core.Endpoint) (int64, error) { func (s *Store) getEndpointID(tx *sql.Tx, ep *endpoint.Endpoint) (int64, error) {
var id int64 var id int64
err := tx.QueryRow("SELECT endpoint_id FROM endpoints WHERE endpoint_key = $1", endpoint.Key()).Scan(&id) err := tx.QueryRow("SELECT endpoint_id FROM endpoints WHERE endpoint_key = $1", ep.Key()).Scan(&id)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return 0, common.ErrEndpointNotFound return 0, common.ErrEndpointNotFound

View file

@ -4,31 +4,31 @@ import (
"testing" "testing"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
) )
var ( var (
firstCondition = core.Condition("[STATUS] == 200") firstCondition = endpoint.Condition("[STATUS] == 200")
secondCondition = core.Condition("[RESPONSE_TIME] < 500") secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h") thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
now = time.Now() now = time.Now()
testEndpoint = core.Endpoint{ testEndpoint = endpoint.Endpoint{
Name: "name", Name: "name",
Group: "group", Group: "group",
URL: "https://example.org/what/ever", URL: "https://example.org/what/ever",
Method: "GET", Method: "GET",
Body: "body", Body: "body",
Interval: 30 * time.Second, Interval: 30 * time.Second,
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition}, Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
Alerts: nil, Alerts: nil,
NumberOfFailuresInARow: 0, NumberOfFailuresInARow: 0,
NumberOfSuccessesInARow: 0, NumberOfSuccessesInARow: 0,
} }
testSuccessfulResult = core.Result{ testSuccessfulResult = endpoint.Result{
Hostname: "example.org", Hostname: "example.org",
IP: "127.0.0.1", IP: "127.0.0.1",
HTTPStatus: 200, HTTPStatus: 200,
@ -38,7 +38,7 @@ var (
Timestamp: now, Timestamp: now,
Duration: 150 * time.Millisecond, Duration: 150 * time.Millisecond,
CertificateExpiration: 10 * time.Hour, CertificateExpiration: 10 * time.Hour,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: true, Success: true,
@ -53,7 +53,7 @@ var (
}, },
}, },
} }
testUnsuccessfulResult = core.Result{ testUnsuccessfulResult = endpoint.Result{
Hostname: "example.org", Hostname: "example.org",
IP: "127.0.0.1", IP: "127.0.0.1",
HTTPStatus: 200, HTTPStatus: 200,
@ -63,7 +63,7 @@ var (
Timestamp: now, Timestamp: now,
Duration: 750 * time.Millisecond, Duration: 750 * time.Millisecond,
CertificateExpiration: 10 * time.Hour, CertificateExpiration: 10 * time.Hour,
ConditionResults: []*core.ConditionResult{ ConditionResults: []*endpoint.ConditionResult{
{ {
Condition: "[STATUS] == 200", Condition: "[STATUS] == 200",
Success: true, Success: true,
@ -100,7 +100,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
now := time.Now().Truncate(time.Hour) now := time.Now().Truncate(time.Hour)
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-5 * time.Hour), Success: true}) store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-5 * time.Hour), Success: true})
tx, _ := store.db.Begin() tx, _ := store.db.Begin()
oldest, _ := store.getAgeOfOldestEndpointUptimeEntry(tx, 1) oldest, _ := store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
@ -110,7 +110,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
} }
// The oldest cache entry should remain at ~5 hours old, because this entry is more recent // The oldest cache entry should remain at ~5 hours old, because this entry is more recent
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-3 * time.Hour), Success: true}) store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-3 * time.Hour), Success: true})
tx, _ = store.db.Begin() tx, _ = store.db.Begin()
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
@ -120,7 +120,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
} }
// The oldest cache entry should now become at ~8 hours old, because this entry is older // The oldest cache entry should now become at ~8 hours old, because this entry is older
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-8 * time.Hour), Success: true}) store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})
tx, _ = store.db.Begin() tx, _ = store.db.Begin()
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
@ -130,7 +130,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
} }
// Since this is one hour before reaching the clean up threshold, the oldest entry should now be this one // Since this is one hour before reaching the clean up threshold, the oldest entry should now be this one
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold - time.Hour)), Success: true}) store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold - time.Hour)), Success: true})
tx, _ = store.db.Begin() tx, _ = store.db.Begin()
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
@ -141,7 +141,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
// Since this entry is after the uptimeCleanUpThreshold, both this entry as well as the previous // Since this entry is after the uptimeCleanUpThreshold, both this entry as well as the previous
// one should be deleted since they both surpass uptimeRetention // one should be deleted since they both surpass uptimeRetention
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold + time.Hour)), Success: true}) store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold + time.Hour)), Success: true})
tx, _ = store.db.Begin() tx, _ = store.db.Begin()
oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
@ -313,7 +313,7 @@ func TestStore_InvalidTransaction(t *testing.T) {
if _, err := store.insertEndpoint(tx, &testEndpoint); err == nil { if _, err := store.insertEndpoint(tx, &testEndpoint); err == nil {
t.Error("should've returned an error, because the transaction was already committed") t.Error("should've returned an error, because the transaction was already committed")
} }
if err := store.insertEndpointEvent(tx, 1, core.NewEventFromResult(&testSuccessfulResult)); err == nil { if err := store.insertEndpointEvent(tx, 1, endpoint.NewEventFromResult(&testSuccessfulResult)); err == nil {
t.Error("should've returned an error, because the transaction was already committed") t.Error("should've returned an error, because the transaction was already committed")
} }
if err := store.insertEndpointResult(tx, 1, &testSuccessfulResult); err == nil { if err := store.insertEndpointResult(tx, 1, &testSuccessfulResult); err == nil {

View file

@ -5,7 +5,7 @@ import (
"log" "log"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage" "github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
"github.com/TwiN/gatus/v5/storage/store/memory" "github.com/TwiN/gatus/v5/storage/store/memory"
@ -14,15 +14,15 @@ import (
// Store is the interface that each store should implement // Store is the interface that each store should implement
type Store interface { type Store interface {
// GetAllEndpointStatuses returns the JSON encoding of all monitored core.EndpointStatus // GetAllEndpointStatuses returns the JSON encoding of all monitored endpoint.Status
// with a subset of core.Result defined by the page and pageSize parameters // with a subset of endpoint.Result defined by the page and pageSize parameters
GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*core.EndpointStatus, error) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error)
// GetEndpointStatus returns the endpoint status for a given endpoint name in the given group // GetEndpointStatus returns the endpoint status for a given endpoint name in the given group
GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error)
// GetEndpointStatusByKey returns the endpoint status for a given key // GetEndpointStatusByKey returns the endpoint status for a given key
GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*core.EndpointStatus, error) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*endpoint.Status, error)
// GetUptimeByKey returns the uptime percentage during a time range // GetUptimeByKey returns the uptime percentage during a time range
GetUptimeByKey(key string, from, to time.Time) (float64, error) GetUptimeByKey(key string, from, to time.Time) (float64, error)
@ -34,9 +34,9 @@ type Store interface {
GetHourlyAverageResponseTimeByKey(key string, from, to time.Time) (map[int64]int, error) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time) (map[int64]int, error)
// Insert adds the observed result for the specified endpoint into the store // Insert adds the observed result for the specified endpoint into the store
Insert(endpoint *core.Endpoint, result *core.Result) error Insert(ep *endpoint.Endpoint, result *endpoint.Result) error
// DeleteAllEndpointStatusesNotInKeys removes all EndpointStatus that are not within the keys provided // DeleteAllEndpointStatusesNotInKeys removes all Status that are not within the keys provided
// //
// Used to delete endpoints that have been persisted but are no longer part of the configured endpoints // Used to delete endpoints that have been persisted but are no longer part of the configured endpoints
DeleteAllEndpointStatusesNotInKeys(keys []string) int DeleteAllEndpointStatusesNotInKeys(keys []string) int

View file

@ -5,7 +5,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/TwiN/gatus/v5/core" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/common/paging"
"github.com/TwiN/gatus/v5/storage/store/memory" "github.com/TwiN/gatus/v5/storage/store/memory"
"github.com/TwiN/gatus/v5/storage/store/sql" "github.com/TwiN/gatus/v5/storage/store/sql"
@ -53,11 +53,11 @@ func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) {
for _, numberOfEndpointsToCreate := range numberOfEndpoints { for _, numberOfEndpointsToCreate := range numberOfEndpoints {
// Create endpoints and insert results // Create endpoints and insert results
for i := 0; i < numberOfEndpointsToCreate; i++ { for i := 0; i < numberOfEndpointsToCreate; i++ {
endpoint := testEndpoint ep := testEndpoint
endpoint.Name = "endpoint" + strconv.Itoa(i) ep.Name = "endpoint" + strconv.Itoa(i)
// Insert 20 results for each endpoint // Insert 20 results for each endpoint
for j := 0; j < 20; j++ { for j := 0; j < 20; j++ {
scenario.Store.Insert(&endpoint, &testSuccessfulResult) scenario.Store.Insert(&ep, &testSuccessfulResult)
} }
} }
// Run the scenarios // Run the scenarios
@ -123,7 +123,7 @@ func BenchmarkStore_Insert(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
n := 0 n := 0
for pb.Next() { for pb.Next() {
var result core.Result var result endpoint.Result
if n%10 == 0 { if n%10 == 0 {
result = testUnsuccessfulResult result = testUnsuccessfulResult
} else { } else {
@ -136,7 +136,7 @@ func BenchmarkStore_Insert(b *testing.B) {
}) })
} else { } else {
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
var result core.Result var result endpoint.Result
if n%10 == 0 { if n%10 == 0 {
result = testUnsuccessfulResult result = testUnsuccessfulResult
} else { } else {

Some files were not shown because too many files have changed in this diff Show more