Add endpoints.onErrorAdd

Add endpoints[].onErrorAdd to add response body or parts of it as error, when the conditions fail for easier debugging.

Implements https://github.com/TwiN/gatus/issues/742
This commit is contained in:
Philipp Kolmann 2024-04-23 11:37:05 +02:00
parent 28339684bf
commit 01c473415e
3 changed files with 50 additions and 0 deletions

View File

@ -272,6 +272,7 @@ You can then configure alerts to be triggered when an endpoint is unhealthy once
| `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` | | `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` |
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` | | `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
| `endpoints[].ui.badge.reponse-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` | | `endpoints[].ui.badge.reponse-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
| `endpoints[].onErrorAdd` | Add whole response Body ("[BODY]") or part of JSON ("[BODY].error") when an error occurs. | `""` |
### External Endpoints ### External Endpoints

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/TwiN/gatus/v5/jsonpath"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -120,6 +121,9 @@ type Endpoint struct {
// NumberOfSuccessesInARow is the number of successful evaluations in a row // NumberOfSuccessesInARow is the number of successful evaluations in a row
NumberOfSuccessesInARow int `yaml:"-"` NumberOfSuccessesInARow int `yaml:"-"`
// OnErrorAdd is the message or [BODY] or part of [BODY].json to add to the result's errors if the endpoint fails
OnErrorAdd string `yaml:"onErrorAdd,omitempty"`
} }
// IsEnabled returns whether the endpoint is enabled or not // IsEnabled returns whether the endpoint is enabled or not
@ -302,6 +306,25 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
if endpoint.UIConfig.HideConditions { if endpoint.UIConfig.HideConditions {
result.ConditionResults = nil result.ConditionResults = nil
} }
if endpoint.OnErrorAdd != "" && !result.Success {
if endpoint.OnErrorAdd == BodyPlaceholder && len(result.Body) > 0 {
result.Errors = append(result.Errors, string(result.Body))
} else if strings.Contains(endpoint.OnErrorAdd, BodyPlaceholder) && len(result.Body) > 0 {
resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.TrimPrefix(
strings.TrimPrefix(endpoint.OnErrorAdd, BodyPlaceholder), "."), result.Body)
if err != nil {
result.Errors = append(result.Errors, "Decoding of endpoint.OnErrorAdd failed: "+err.Error())
} else {
if resolvedElementLength > 0 {
result.Errors = append(result.Errors, resolvedElement)
}
}
} else {
result.Errors = append(result.Errors, endpoint.OnErrorAdd)
}
}
return result return result
} }
@ -417,6 +440,9 @@ 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 (endpoint *Endpoint) needsToReadBody() bool {
if strings.Contains(endpoint.OnErrorAdd, BodyPlaceholder) {
return true
}
for _, condition := range endpoint.Conditions { for _, condition := range endpoint.Conditions {
if condition.hasBodyPlaceholder() { if condition.hasBodyPlaceholder() {
return true return true

View File

@ -204,6 +204,29 @@ func TestEndpoint(t *testing.T) {
}, },
MockRoundTripper: nil, MockRoundTripper: nil,
}, },
{
Name: "failed-status-with-error-from-body",
Endpoint: Endpoint{
Name: "website-health",
URL: "https://twin.sh/health",
Conditions: []Condition{"[STATUS] == 200"},
OnErrorAdd: "[BODY].error",
},
ExpectedResult: &Result{
Success: false,
Connected: true,
Hostname: "twin.sh",
ConditionResults: []*ConditionResult{
{Condition: "[STATUS] (502) == 200", Success: false},
},
DomainExpiration: 0, // Because there's no [DOMAIN_EXPIRATION] condition, this is not resolved, so it should be 0.
Errors: []string{"something went wrong"},
},
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusBadGateway, Body: io.NopCloser(strings.NewReader(
"{\"error\":\"something went wrong\"}"))}
}),
},
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {