gatus/core/condition.go

208 lines
8.0 KiB
Go
Raw Normal View History

package core
import (
"fmt"
"log"
2020-10-05 02:00:24 +02:00
"strconv"
"strings"
2020-11-15 18:26:35 +01:00
"time"
2020-11-16 16:10:02 +01:00
"github.com/TwinProduction/gatus/jsonpath"
"github.com/TwinProduction/gatus/pattern"
)
2020-10-05 02:00:24 +02:00
const (
2020-10-07 01:07:47 +02:00
// StatusPlaceholder is a placeholder for a HTTP status.
//
// Values that could replace the placeholder: 200, 404, 500, ...
StatusPlaceholder = "[STATUS]"
2020-11-18 01:34:22 +01:00
// IPPlaceholder is a placeholder for an IP.
2020-10-07 01:07:47 +02:00
//
// Values that could replace the placeholder: 127.0.0.1, 10.0.0.1, ...
2020-11-18 01:34:22 +01:00
IPPlaceholder = "[IP]"
2020-10-07 01:07:47 +02:00
2020-11-19 09:31:30 +01:00
// DNSRCodePlaceholder is a place holder for DNS_RCODE
2020-11-18 00:55:31 +01:00
//
// Values that could be NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP and REFUSED
2020-11-19 09:31:30 +01:00
DNSRCodePlaceholder = "[DNS_RCODE]"
2020-11-18 00:55:31 +01:00
2020-11-18 01:34:22 +01:00
// ResponseTimePlaceholder is a placeholder for the request response time, in milliseconds.
2020-10-07 01:07:47 +02:00
//
// Values that could replace the placeholder: 1, 500, 1000, ...
2020-11-18 01:34:22 +01:00
ResponseTimePlaceholder = "[RESPONSE_TIME]"
2020-10-07 01:07:47 +02:00
2020-11-18 01:34:22 +01:00
// BodyPlaceholder is a placeholder for the body of the response
2020-10-07 01:07:47 +02:00
//
// Values that could replace the placeholder: {}, {"data":{"name":"john"}}, ...
2020-11-18 01:34:22 +01:00
BodyPlaceholder = "[BODY]"
2020-10-07 01:07:47 +02:00
2020-11-18 01:34:22 +01:00
// ConnectedPlaceholder is a placeholder for whether a connection was successfully established.
2020-10-07 01:07:47 +02:00
//
// Values that could replace the placeholder: true, false
2020-11-18 01:34:22 +01:00
ConnectedPlaceholder = "[CONNECTED]"
2020-10-05 02:00:24 +02:00
// CertificateExpirationPlaceholder is a placeholder for the duration before certificate expiration, in milliseconds.
//
// Values that could replace the placeholder: 4461677039 (~52 days)
CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]"
2020-10-23 22:31:49 +02:00
// LengthFunctionPrefix is the prefix for the length function
LengthFunctionPrefix = "len("
2020-10-23 22:35:16 +02:00
// PatternFunctionPrefix is the prefix for the pattern function
2020-10-05 02:00:24 +02:00
PatternFunctionPrefix = "pat("
2020-10-23 22:35:16 +02:00
// FunctionSuffix is the suffix for all functions
2020-10-23 22:31:49 +02:00
FunctionSuffix = ")"
// InvalidConditionElementSuffix is the suffix that will be appended to an invalid condition
2020-10-05 02:00:24 +02:00
InvalidConditionElementSuffix = "(INVALID)"
)
2020-10-07 01:07:47 +02:00
// Condition is a condition that needs to be met in order for a Service to be considered healthy.
type Condition string
2020-10-02 01:57:11 +02:00
// evaluate the Condition with the Result of the health check
func (c *Condition) evaluate(result *Result) bool {
condition := string(*c)
success := false
var resolvedCondition string
if strings.Contains(condition, "==") {
parts := sanitizeAndResolve(strings.Split(condition, "=="), result)
2020-10-02 01:57:11 +02:00
success = isEqual(parts[0], parts[1])
resolvedCondition = fmt.Sprintf("%v == %v", parts[0], parts[1])
} else if strings.Contains(condition, "!=") {
parts := sanitizeAndResolve(strings.Split(condition, "!="), result)
2020-10-02 01:57:11 +02:00
success = !isEqual(parts[0], parts[1])
resolvedCondition = fmt.Sprintf("%v != %v", parts[0], parts[1])
} else if strings.Contains(condition, "<=") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
success = parts[0] <= parts[1]
resolvedCondition = fmt.Sprintf("%v <= %v", parts[0], parts[1])
} else if strings.Contains(condition, ">=") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
success = parts[0] >= parts[1]
resolvedCondition = fmt.Sprintf("%v >= %v", parts[0], parts[1])
} else if strings.Contains(condition, ">") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
success = parts[0] > parts[1]
resolvedCondition = fmt.Sprintf("%v > %v", parts[0], parts[1])
} else if strings.Contains(condition, "<") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
success = parts[0] < parts[1]
resolvedCondition = fmt.Sprintf("%v < %v", parts[0], parts[1])
} else {
result.Errors = append(result.Errors, fmt.Sprintf("invalid condition '%s' has been provided", condition))
return false
}
conditionToDisplay := condition
// If the condition isn't a success, return what the resolved condition was too
if !success {
log.Printf("[Condition][evaluate] Condition '%s' did not succeed because '%s' is false", condition, resolvedCondition)
// Check if the resolved condition was an invalid path
isResolvedConditionInvalidPath := strings.ReplaceAll(resolvedCondition, fmt.Sprintf("%s ", InvalidConditionElementSuffix), "") == condition
if isResolvedConditionInvalidPath {
// Since, in the event of an invalid path, the resolvedCondition contains the condition itself,
// we'll only display the resolvedCondition
conditionToDisplay = resolvedCondition
} else {
conditionToDisplay = fmt.Sprintf("%s (%s)", condition, resolvedCondition)
}
}
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
return success
}
2020-10-02 01:57:11 +02:00
// isEqual compares two strings.
//
// It also supports the pattern function. That is to say, if one of the strings starts with PatternFunctionPrefix
// and ends with FunctionSuffix, it will be treated like a pattern.
func isEqual(first, second string) bool {
var isFirstPattern, isSecondPattern bool
if strings.HasPrefix(first, PatternFunctionPrefix) && strings.HasSuffix(first, FunctionSuffix) {
isFirstPattern = true
first = strings.TrimSuffix(strings.TrimPrefix(first, PatternFunctionPrefix), FunctionSuffix)
}
if strings.HasPrefix(second, PatternFunctionPrefix) && strings.HasSuffix(second, FunctionSuffix) {
isSecondPattern = true
second = strings.TrimSuffix(strings.TrimPrefix(second, PatternFunctionPrefix), FunctionSuffix)
}
if isFirstPattern && !isSecondPattern {
return pattern.Match(first, second)
} else if !isFirstPattern && isSecondPattern {
return pattern.Match(second, first)
} else {
return first == second
}
}
2020-10-05 02:00:24 +02:00
// sanitizeAndResolve sanitizes and resolves a list of element and returns the list of resolved elements
func sanitizeAndResolve(list []string, result *Result) []string {
var sanitizedList []string
body := strings.TrimSpace(string(result.Body))
for _, element := range list {
element = strings.TrimSpace(element)
switch strings.ToUpper(element) {
case StatusPlaceholder:
2020-10-23 22:29:20 +02:00
element = strconv.Itoa(result.HTTPStatus)
2020-11-18 01:34:22 +01:00
case IPPlaceholder:
2020-10-23 22:29:20 +02:00
element = result.IP
2020-11-18 01:34:22 +01:00
case ResponseTimePlaceholder:
2020-10-05 02:00:24 +02:00
element = strconv.Itoa(int(result.Duration.Milliseconds()))
2020-11-18 01:34:22 +01:00
case BodyPlaceholder:
2020-10-05 02:00:24 +02:00
element = body
2020-11-19 09:31:30 +01:00
case DNSRCodePlaceholder:
2020-11-18 00:55:31 +01:00
element = result.DNSRCode
2020-11-18 01:34:22 +01:00
case ConnectedPlaceholder:
2020-10-05 02:00:24 +02:00
element = strconv.FormatBool(result.Connected)
case CertificateExpirationPlaceholder:
2020-11-18 01:34:22 +01:00
element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10)
2020-10-05 02:00:24 +02:00
default:
2020-11-18 01:34:22 +01:00
// if contains the BodyPlaceholder, then evaluate json path
if strings.Contains(element, BodyPlaceholder) {
2020-10-05 02:00:24 +02:00
wantLength := false
if strings.HasPrefix(element, LengthFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) {
wantLength = true
element = strings.TrimSuffix(strings.TrimPrefix(element, LengthFunctionPrefix), FunctionSuffix)
}
2020-11-18 01:34:22 +01:00
resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceholder), "", 1), result.Body)
2020-10-05 02:00:24 +02:00
if err != nil {
if err.Error() != "unexpected end of JSON input" {
result.Errors = append(result.Errors, err.Error())
}
if wantLength {
2021-01-05 00:00:36 +01:00
element = fmt.Sprintf("%s%s%s %s", LengthFunctionPrefix, element, FunctionSuffix, InvalidConditionElementSuffix)
} else {
element = fmt.Sprintf("%s %s", element, InvalidConditionElementSuffix)
}
2020-10-05 02:00:24 +02:00
} else {
if wantLength {
element = fmt.Sprintf("%d", resolvedElementLength)
} else {
element = resolvedElement
}
}
}
}
sanitizedList = append(sanitizedList, element)
}
return sanitizedList
}
2020-11-15 16:50:05 +01:00
func sanitizeAndResolveNumerical(list []string, result *Result) []int64 {
var sanitizedNumbers []int64
2020-10-05 02:00:24 +02:00
sanitizedList := sanitizeAndResolve(list, result)
for _, element := range sanitizedList {
2020-11-16 16:10:02 +01:00
if duration, err := time.ParseDuration(element); duration != 0 && err == nil {
2020-11-15 18:26:35 +01:00
sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds())
} else if number, err := strconv.ParseInt(element, 10, 64); err != nil {
2020-10-05 02:00:24 +02:00
// Default to 0 if the string couldn't be converted to an integer
sanitizedNumbers = append(sanitizedNumbers, 0)
} else {
sanitizedNumbers = append(sanitizedNumbers, number)
}
}
return sanitizedNumbers
}