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

Add support for [RESPONSE_TIME] and >, <, <=, >= operators

This commit is contained in:
TwinProduction 2020-04-10 16:34:20 -04:00
parent 1701ced07b
commit 92cb9c86d1
4 changed files with 114 additions and 35 deletions

View file

@ -19,9 +19,10 @@ metrics: true # Whether to expose metrics at /metrics
services:
- name: twinnation # Name of your service, can be anything
url: https://twinnation.org/health
interval: 15s # Duration to wait between every status check (opt. default: 10s)
interval: 15s # Duration to wait between every status check (default: 10s)
conditions:
- "[STATUS] == 200"
- "[RESPONSE_TIME] < 300"
- name: github
url: https://api.github.com/healthz
conditions:
@ -31,6 +32,19 @@ services:
Note that you can also add environment variables in the your configuration file (i.e. `$DOMAIN`, `${DOMAIN}`)
### Conditions
Here are some examples of conditions you can use:
| Condition | Description | Values that would pass | Values that would fail |
| ------------------------------------- | ----------------------------------------- | ---------------------- | ---------------------- |
| `[STATUS] == 200` | Status must be equal to 200 | 200 | 201, 404, 500 |
| `[STATUS] < 300` | Status must lower than 300 | 200, 201, 299 | 301, 302, 400, 500 |
| `[STATUS] <= 299` | Status must be less than or equal to 299 | 200, 201, 299 | 301, 302, 400, 500 |
| `[STATUS] > 400` | Status must be greater than 400 | 401, 402, 403, 404 | 200, 201, 300, 400 |
| `[RESPONSE_TIME] < 500` | Response time must be below 500ms | 100ms, 200ms, 300ms | 500ms, 1500ms |
## Docker
Building the Docker image is done as following:

View file

@ -2,9 +2,10 @@ metrics: true
services:
- name: Twinnation
url: https://twinnation.org/health
interval: 30s
interval: 10s
conditions:
- "[STATUS] == 200"
- "[RESPONSE_TIME] < 20"
- name: GitHub API
url: https://api.github.com/healthz
interval: 30s

View file

@ -10,6 +10,12 @@ import (
"time"
)
const (
StatusPlaceholder = "[STATUS]"
IPPlaceHolder = "[IP]"
ResponseTimePlaceHolder = "[RESPONSE_TIME]"
)
type HealthStatus struct {
Status string `json:"status"`
Message string `json:"message,omitempty"`
@ -95,44 +101,31 @@ type Condition string
func (c *Condition) evaluate(result *Result) bool {
condition := string(*c)
success := false
if strings.Contains(condition, "==") {
parts := sanitizeAndResolve(strings.Split(condition, "=="), result)
if parts[0] == parts[1] {
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
Condition: c,
Success: true,
Explanation: fmt.Sprintf("%s is equal to %s", parts[0], parts[1]),
})
return true
} else {
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
Condition: c,
Success: false,
Explanation: fmt.Sprintf("%s is not equal to %s", parts[0], parts[1]),
})
return false
}
success = parts[0] == parts[1]
} else if strings.Contains(condition, "!=") {
parts := sanitizeAndResolve(strings.Split(condition, "!="), result)
if parts[0] != parts[1] {
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
Condition: c,
Success: true,
Explanation: fmt.Sprintf("%s is not equal to %s", parts[0], parts[1]),
})
return true
} else {
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
Condition: c,
Success: false,
Explanation: fmt.Sprintf("%s is equal to %s", parts[0], parts[1]),
})
return false
}
success = parts[0] != parts[1]
} else if strings.Contains(condition, "<=") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
success = parts[0] <= parts[1]
} else if strings.Contains(condition, ">=") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
success = parts[0] >= parts[1]
} else if strings.Contains(condition, ">") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
success = parts[0] > parts[1]
} else if strings.Contains(condition, "<") {
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
success = parts[0] < parts[1]
} else {
result.Errors = append(result.Errors, fmt.Sprintf("invalid condition '%s' has been provided", condition))
return false
}
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: c, Success: success})
return success
}
func sanitizeAndResolve(list []string, result *Result) []string {
@ -140,13 +133,29 @@ func sanitizeAndResolve(list []string, result *Result) []string {
for _, element := range list {
element = strings.TrimSpace(element)
switch strings.ToUpper(element) {
case "[STATUS]":
case StatusPlaceholder:
element = strconv.Itoa(result.HttpStatus)
case "[IP]":
case IPPlaceHolder:
element = result.Ip
case ResponseTimePlaceHolder:
element = strconv.Itoa(int(result.Duration.Milliseconds()))
default:
}
sanitizedList = append(sanitizedList, element)
}
return sanitizedList
}
func sanitizeAndResolveNumerical(list []string, result *Result) []int {
var sanitizedNumbers []int
sanitizedList := sanitizeAndResolve(list, result)
for _, element := range sanitizedList {
if number, err := strconv.Atoi(element); 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
}

View file

@ -2,6 +2,7 @@ package core
import (
"testing"
"time"
)
func TestEvaluateWithIp(t *testing.T) {
@ -22,7 +23,7 @@ func TestEvaluateWithStatus(t *testing.T) {
}
}
func TestEvaluateWithFailure(t *testing.T) {
func TestEvaluateWithStatusFailure(t *testing.T) {
condition := Condition("[STATUS] == 200")
result := &Result{HttpStatus: 500}
condition.evaluate(result)
@ -31,6 +32,60 @@ func TestEvaluateWithFailure(t *testing.T) {
}
}
func TestEvaluateWithStatusUsingLessThan(t *testing.T) {
condition := Condition("[STATUS] < 300")
result := &Result{HttpStatus: 201}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestEvaluateWithStatusFailureUsingLessThan(t *testing.T) {
condition := Condition("[STATUS] < 300")
result := &Result{HttpStatus: 404}
condition.evaluate(result)
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
}
func TestEvaluateWithResponseTimeUsingLessThan(t *testing.T) {
condition := Condition("[RESPONSE_TIME] < 500")
result := &Result{Duration: time.Millisecond * 50}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestEvaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
condition := Condition("[RESPONSE_TIME] > 500")
result := &Result{Duration: time.Millisecond * 750}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestEvaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.T) {
condition := Condition("[RESPONSE_TIME] >= 500")
result := &Result{Duration: time.Millisecond * 500}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestEvaluateWithResponseTimeUsingLessThanOrEqualTo(t *testing.T) {
condition := Condition("[RESPONSE_TIME] <= 500")
result := &Result{Duration: time.Millisecond * 500}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestIntegrationEvaluateConditions(t *testing.T) {
condition := Condition("[STATUS] == 200")
service := Service{