mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-23 15:18:55 +01:00
208 lines
8.0 KiB
Go
208 lines
8.0 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/TwinProduction/gatus/jsonpath"
|
|
"github.com/TwinProduction/gatus/pattern"
|
|
)
|
|
|
|
const (
|
|
// StatusPlaceholder is a placeholder for a HTTP status.
|
|
//
|
|
// Values that could replace the placeholder: 200, 404, 500, ...
|
|
StatusPlaceholder = "[STATUS]"
|
|
|
|
// IPPlaceholder is a placeholder for an IP.
|
|
//
|
|
// Values that could replace the placeholder: 127.0.0.1, 10.0.0.1, ...
|
|
IPPlaceholder = "[IP]"
|
|
|
|
// DNSRCodePlaceholder is a place holder for DNS_RCODE
|
|
//
|
|
// Values that could be NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP and REFUSED
|
|
DNSRCodePlaceholder = "[DNS_RCODE]"
|
|
|
|
// ResponseTimePlaceholder is a placeholder for the request response time, in milliseconds.
|
|
//
|
|
// Values that could replace the placeholder: 1, 500, 1000, ...
|
|
ResponseTimePlaceholder = "[RESPONSE_TIME]"
|
|
|
|
// BodyPlaceholder is a placeholder for the body of the response
|
|
//
|
|
// Values that could replace the placeholder: {}, {"data":{"name":"john"}}, ...
|
|
BodyPlaceholder = "[BODY]"
|
|
|
|
// ConnectedPlaceholder is a placeholder for whether a connection was successfully established.
|
|
//
|
|
// Values that could replace the placeholder: true, false
|
|
ConnectedPlaceholder = "[CONNECTED]"
|
|
|
|
// CertificateExpirationPlaceholder is a placeholder for the duration before certificate expiration, in milliseconds.
|
|
//
|
|
// Values that could replace the placeholder: 4461677039 (~52 days)
|
|
CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]"
|
|
|
|
// LengthFunctionPrefix is the prefix for the length function
|
|
LengthFunctionPrefix = "len("
|
|
|
|
// PatternFunctionPrefix is the prefix for the pattern function
|
|
PatternFunctionPrefix = "pat("
|
|
|
|
// FunctionSuffix is the suffix for all functions
|
|
FunctionSuffix = ")"
|
|
|
|
// InvalidConditionElementSuffix is the suffix that will be appended to an invalid condition
|
|
InvalidConditionElementSuffix = "(INVALID)"
|
|
)
|
|
|
|
// Condition is a condition that needs to be met in order for a Service to be considered healthy.
|
|
type Condition string
|
|
|
|
// 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)
|
|
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)
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
element = strconv.Itoa(result.HTTPStatus)
|
|
case IPPlaceholder:
|
|
element = result.IP
|
|
case ResponseTimePlaceholder:
|
|
element = strconv.Itoa(int(result.Duration.Milliseconds()))
|
|
case BodyPlaceholder:
|
|
element = body
|
|
case DNSRCodePlaceholder:
|
|
element = result.DNSRCode
|
|
case ConnectedPlaceholder:
|
|
element = strconv.FormatBool(result.Connected)
|
|
case CertificateExpirationPlaceholder:
|
|
element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10)
|
|
default:
|
|
// if contains the BodyPlaceholder, then evaluate json path
|
|
if strings.Contains(element, BodyPlaceholder) {
|
|
wantLength := false
|
|
if strings.HasPrefix(element, LengthFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) {
|
|
wantLength = true
|
|
element = strings.TrimSuffix(strings.TrimPrefix(element, LengthFunctionPrefix), FunctionSuffix)
|
|
}
|
|
resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceholder), "", 1), result.Body)
|
|
if err != nil {
|
|
if err.Error() != "unexpected end of JSON input" {
|
|
result.Errors = append(result.Errors, err.Error())
|
|
}
|
|
if wantLength {
|
|
element = fmt.Sprintf("len(%s) %s", element, InvalidConditionElementSuffix)
|
|
} else {
|
|
element = fmt.Sprintf("%s %s", element, InvalidConditionElementSuffix)
|
|
}
|
|
} else {
|
|
if wantLength {
|
|
element = fmt.Sprintf("%d", resolvedElementLength)
|
|
} else {
|
|
element = resolvedElement
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sanitizedList = append(sanitizedList, element)
|
|
}
|
|
return sanitizedList
|
|
}
|
|
|
|
func sanitizeAndResolveNumerical(list []string, result *Result) []int64 {
|
|
var sanitizedNumbers []int64
|
|
sanitizedList := sanitizeAndResolve(list, result)
|
|
for _, element := range sanitizedList {
|
|
if duration, err := time.ParseDuration(element); duration != 0 && err == nil {
|
|
sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds())
|
|
} else if number, err := strconv.ParseInt(element, 10, 64); err != nil {
|
|
// Default to 0 if the string couldn't be converted to an integer
|
|
sanitizedNumbers = append(sanitizedNumbers, 0)
|
|
} else {
|
|
sanitizedNumbers = append(sanitizedNumbers, number)
|
|
}
|
|
}
|
|
return sanitizedNumbers
|
|
}
|