mirror of
https://github.com/TwiN/gatus.git
synced 2025-01-03 04:29:13 +01:00
Add support for [BODY] placeholder and basic JSON path support
Note that arrays are not currently supported, same with asterisks
This commit is contained in:
parent
15b8f8a293
commit
3e2b56ba89
@ -38,12 +38,14 @@ Note that you can also add environment variables in the your configuration file
|
||||
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 |
|
||||
| `[BODY] == 1` | The body must be equal to 1 | 1 | literally anything else |
|
||||
| (beta) `[BODY].data.id == 1` | The jsonpath `$.data.id` is equal to 1 | `{ "data" : { "id": 1 }` | literally anything else |
|
||||
|
||||
|
||||
## Docker
|
||||
|
@ -5,7 +5,8 @@ services:
|
||||
interval: 10s
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[RESPONSE_TIME] < 30"
|
||||
- "[RESPONSE_TIME] < 500"
|
||||
- "[BODY].status == UP"
|
||||
- name: Example
|
||||
url: https://example.org/
|
||||
interval: 30s
|
||||
|
@ -2,6 +2,8 @@ package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/TwinProduction/gatus/jsonpath"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -14,6 +16,7 @@ const (
|
||||
StatusPlaceholder = "[STATUS]"
|
||||
IPPlaceHolder = "[IP]"
|
||||
ResponseTimePlaceHolder = "[RESPONSE_TIME]"
|
||||
BodyPlaceHolder = "[BODY]"
|
||||
)
|
||||
|
||||
type HealthStatus struct {
|
||||
@ -21,13 +24,9 @@ type HealthStatus struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type ServerMessage struct {
|
||||
Error bool `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
HttpStatus int `json:"status"`
|
||||
Body []byte `json:"-"`
|
||||
Hostname string `json:"hostname"`
|
||||
Ip string `json:"-"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
@ -59,7 +58,8 @@ func (service *Service) getIp(result *Result) {
|
||||
result.Ip = ips[0].String()
|
||||
}
|
||||
|
||||
func (service *Service) getStatus(result *Result) {
|
||||
func (service *Service) call(result *Result) {
|
||||
// TODO: re-use the same client instead of creating multiple clients
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
@ -71,13 +71,17 @@ func (service *Service) getStatus(result *Result) {
|
||||
}
|
||||
result.Duration = time.Now().Sub(startTime)
|
||||
result.HttpStatus = response.StatusCode
|
||||
result.Body, err = ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) EvaluateConditions() *Result {
|
||||
result := &Result{Success: true, Errors: []string{}}
|
||||
service.getIp(result)
|
||||
if len(result.Errors) == 0 {
|
||||
service.getStatus(result)
|
||||
service.call(result)
|
||||
} else {
|
||||
result.Success = false
|
||||
}
|
||||
@ -138,7 +142,13 @@ func sanitizeAndResolve(list []string, result *Result) []string {
|
||||
element = result.Ip
|
||||
case ResponseTimePlaceHolder:
|
||||
element = strconv.Itoa(int(result.Duration.Milliseconds()))
|
||||
case BodyPlaceHolder:
|
||||
element = string(result.Body)
|
||||
default:
|
||||
// if starts with BodyPlaceHolder, then do the jsonpath thingy
|
||||
if strings.HasPrefix(element, BodyPlaceHolder) {
|
||||
element = jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceHolder), "", 1), result.Body)
|
||||
}
|
||||
}
|
||||
sanitizedList = append(sanitizedList, element)
|
||||
}
|
||||
|
@ -86,11 +86,83 @@ func TestEvaluateWithResponseTimeUsingLessThanOrEqualTo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWithBody(t *testing.T) {
|
||||
condition := Condition("[BODY] == test")
|
||||
result := &Result{Body: []byte("test")}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWithBodyJsonPath(t *testing.T) {
|
||||
condition := Condition("[BODY].status == UP")
|
||||
result := &Result{Body: []byte("{\"status\":\"UP\"}")}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWithBodyJsonPathComplex(t *testing.T) {
|
||||
condition := Condition("[BODY].data.name == john")
|
||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1, \"name\": \"john\"}}")}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWithBodyJsonPathComplexInt(t *testing.T) {
|
||||
condition := Condition("[BODY].data.id == 1")
|
||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWithBodyJsonPathComplexIntUsingGreaterThan(t *testing.T) {
|
||||
condition := Condition("[BODY].data.id > 0")
|
||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWithBodyJsonPathComplexIntFailureUsingGreaterThan(t *testing.T) {
|
||||
condition := Condition("[BODY].data.id > 5")
|
||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
||||
condition.evaluate(result)
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWithBodyJsonPathComplexIntUsingLessThan(t *testing.T) {
|
||||
condition := Condition("[BODY].data.id < 5")
|
||||
result := &Result{Body: []byte("{\"data\": {\"id\": 2}}")}
|
||||
condition.evaluate(result)
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a success", condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWithBodyJsonPathComplexIntFailureUsingLessThan(t *testing.T) {
|
||||
condition := Condition("[BODY].data.id < 5")
|
||||
result := &Result{Body: []byte("{\"data\": {\"id\": 10}}")}
|
||||
condition.evaluate(result)
|
||||
if result.ConditionResults[0].Success {
|
||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateConditions(t *testing.T) {
|
||||
condition := Condition("[STATUS] == 200")
|
||||
service := Service{
|
||||
Name: "GitHub",
|
||||
Url: "https://api.github.com/healthz",
|
||||
Name: "TwiNNatioN",
|
||||
Url: "https://twinnation.org/health",
|
||||
Conditions: []*Condition{&condition},
|
||||
}
|
||||
result := service.EvaluateConditions()
|
||||
@ -105,8 +177,8 @@ func TestIntegrationEvaluateConditions(t *testing.T) {
|
||||
func TestIntegrationEvaluateConditionsWithFailure(t *testing.T) {
|
||||
condition := Condition("[STATUS] == 500")
|
||||
service := Service{
|
||||
Name: "GitHub",
|
||||
Url: "https://api.github.com/healthz",
|
||||
Name: "TwiNNatioN",
|
||||
Url: "https://twinnation.org/health",
|
||||
Conditions: []*Condition{&condition},
|
||||
}
|
||||
result := service.EvaluateConditions()
|
||||
|
30
jsonpath/jsonpath.go
Normal file
30
jsonpath/jsonpath.go
Normal file
@ -0,0 +1,30 @@
|
||||
package jsonpath
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Eval(path string, b []byte) string {
|
||||
var object map[string]interface{}
|
||||
err := json.Unmarshal(b, &object)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return walk(path, object)
|
||||
}
|
||||
|
||||
func walk(path string, object map[string]interface{}) string {
|
||||
keys := strings.Split(path, ".")
|
||||
targetKey := keys[0]
|
||||
// if there's only one key and the target key is that key, then return its value
|
||||
if len(keys) == 1 {
|
||||
return fmt.Sprintf("%v", object[targetKey])
|
||||
}
|
||||
// if there's more than one key, then walk deeper
|
||||
if len(keys) > 0 {
|
||||
return walk(strings.Replace(path, fmt.Sprintf("%s.", targetKey), "", 1), object[targetKey].(map[string]interface{}))
|
||||
}
|
||||
return ""
|
||||
}
|
Loading…
Reference in New Issue
Block a user