Add support for [RESPONSE_TIME] and >, <, <=, >= operators
This commit is contained in:
parent
1701ced07b
commit
92cb9c86d1
16
README.md
16
README.md
@ -19,9 +19,10 @@ metrics: true # Whether to expose metrics at /metrics
|
|||||||
services:
|
services:
|
||||||
- name: twinnation # Name of your service, can be anything
|
- name: twinnation # Name of your service, can be anything
|
||||||
url: https://twinnation.org/health
|
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:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
|
- "[RESPONSE_TIME] < 300"
|
||||||
- name: github
|
- name: github
|
||||||
url: https://api.github.com/healthz
|
url: https://api.github.com/healthz
|
||||||
conditions:
|
conditions:
|
||||||
@ -31,6 +32,19 @@ services:
|
|||||||
Note that you can also add environment variables in the your configuration file (i.e. `$DOMAIN`, `${DOMAIN}`)
|
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
|
## Docker
|
||||||
|
|
||||||
Building the Docker image is done as following:
|
Building the Docker image is done as following:
|
||||||
|
@ -2,9 +2,10 @@ metrics: true
|
|||||||
services:
|
services:
|
||||||
- name: Twinnation
|
- name: Twinnation
|
||||||
url: https://twinnation.org/health
|
url: https://twinnation.org/health
|
||||||
interval: 30s
|
interval: 10s
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
|
- "[RESPONSE_TIME] < 20"
|
||||||
- name: GitHub API
|
- name: GitHub API
|
||||||
url: https://api.github.com/healthz
|
url: https://api.github.com/healthz
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
@ -10,6 +10,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusPlaceholder = "[STATUS]"
|
||||||
|
IPPlaceHolder = "[IP]"
|
||||||
|
ResponseTimePlaceHolder = "[RESPONSE_TIME]"
|
||||||
|
)
|
||||||
|
|
||||||
type HealthStatus struct {
|
type HealthStatus struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
@ -95,44 +101,31 @@ type Condition string
|
|||||||
|
|
||||||
func (c *Condition) evaluate(result *Result) bool {
|
func (c *Condition) evaluate(result *Result) bool {
|
||||||
condition := string(*c)
|
condition := string(*c)
|
||||||
|
success := false
|
||||||
if strings.Contains(condition, "==") {
|
if strings.Contains(condition, "==") {
|
||||||
parts := sanitizeAndResolve(strings.Split(condition, "=="), result)
|
parts := sanitizeAndResolve(strings.Split(condition, "=="), result)
|
||||||
if parts[0] == parts[1] {
|
success = 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
|
|
||||||
}
|
|
||||||
} else if strings.Contains(condition, "!=") {
|
} else if strings.Contains(condition, "!=") {
|
||||||
parts := sanitizeAndResolve(strings.Split(condition, "!="), result)
|
parts := sanitizeAndResolve(strings.Split(condition, "!="), result)
|
||||||
if parts[0] != parts[1] {
|
success = parts[0] != parts[1]
|
||||||
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
|
} else if strings.Contains(condition, "<=") {
|
||||||
Condition: c,
|
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
|
||||||
Success: true,
|
success = parts[0] <= parts[1]
|
||||||
Explanation: fmt.Sprintf("%s is not equal to %s", parts[0], parts[1]),
|
} else if strings.Contains(condition, ">=") {
|
||||||
})
|
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
|
||||||
return true
|
success = parts[0] >= parts[1]
|
||||||
} else {
|
} else if strings.Contains(condition, ">") {
|
||||||
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
|
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
|
||||||
Condition: c,
|
success = parts[0] > parts[1]
|
||||||
Success: false,
|
} else if strings.Contains(condition, "<") {
|
||||||
Explanation: fmt.Sprintf("%s is equal to %s", parts[0], parts[1]),
|
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
|
||||||
})
|
success = parts[0] < parts[1]
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("invalid condition '%s' has been provided", condition))
|
result.Errors = append(result.Errors, fmt.Sprintf("invalid condition '%s' has been provided", condition))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: c, Success: success})
|
||||||
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeAndResolve(list []string, result *Result) []string {
|
func sanitizeAndResolve(list []string, result *Result) []string {
|
||||||
@ -140,13 +133,29 @@ func sanitizeAndResolve(list []string, result *Result) []string {
|
|||||||
for _, element := range list {
|
for _, element := range list {
|
||||||
element = strings.TrimSpace(element)
|
element = strings.TrimSpace(element)
|
||||||
switch strings.ToUpper(element) {
|
switch strings.ToUpper(element) {
|
||||||
case "[STATUS]":
|
case StatusPlaceholder:
|
||||||
element = strconv.Itoa(result.HttpStatus)
|
element = strconv.Itoa(result.HttpStatus)
|
||||||
case "[IP]":
|
case IPPlaceHolder:
|
||||||
element = result.Ip
|
element = result.Ip
|
||||||
|
case ResponseTimePlaceHolder:
|
||||||
|
element = strconv.Itoa(int(result.Duration.Milliseconds()))
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
sanitizedList = append(sanitizedList, element)
|
sanitizedList = append(sanitizedList, element)
|
||||||
}
|
}
|
||||||
return sanitizedList
|
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
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEvaluateWithIp(t *testing.T) {
|
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")
|
condition := Condition("[STATUS] == 200")
|
||||||
result := &Result{HttpStatus: 500}
|
result := &Result{HttpStatus: 500}
|
||||||
condition.evaluate(result)
|
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) {
|
func TestIntegrationEvaluateConditions(t *testing.T) {
|
||||||
condition := Condition("[STATUS] == 200")
|
condition := Condition("[STATUS] == 200")
|
||||||
service := Service{
|
service := Service{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user