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
16
README.md
16
README.md
@ -37,13 +37,15 @@ Note that you can also add environment variables in the your configuration file
|
|||||||
|
|
||||||
Here are some examples of conditions you can use:
|
Here are some examples of conditions you can use:
|
||||||
|
|
||||||
| Condition | Description | Values that would pass | Values that would fail |
|
| Condition | Description | Values that would pass | Values that would fail |
|
||||||
| ------------------------------------- | ----------------------------------------- | ---------------------- | ---------------------- |
|
| ------------------------------------- | ----------------------------------------- | ------------------------ | ----------------------- |
|
||||||
| `[STATUS] == 200` | Status must be equal to 200 | 200 | 201, 404, 500 |
|
| `[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] < 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] <= 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 |
|
| `[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 |
|
| `[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
|
## Docker
|
||||||
|
@ -5,7 +5,8 @@ services:
|
|||||||
interval: 10s
|
interval: 10s
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
- "[RESPONSE_TIME] < 30"
|
- "[RESPONSE_TIME] < 500"
|
||||||
|
- "[BODY].status == UP"
|
||||||
- name: Example
|
- name: Example
|
||||||
url: https://example.org/
|
url: https://example.org/
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
@ -2,6 +2,8 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/TwinProduction/gatus/jsonpath"
|
||||||
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -14,6 +16,7 @@ const (
|
|||||||
StatusPlaceholder = "[STATUS]"
|
StatusPlaceholder = "[STATUS]"
|
||||||
IPPlaceHolder = "[IP]"
|
IPPlaceHolder = "[IP]"
|
||||||
ResponseTimePlaceHolder = "[RESPONSE_TIME]"
|
ResponseTimePlaceHolder = "[RESPONSE_TIME]"
|
||||||
|
BodyPlaceHolder = "[BODY]"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthStatus struct {
|
type HealthStatus struct {
|
||||||
@ -21,13 +24,9 @@ type HealthStatus struct {
|
|||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerMessage struct {
|
|
||||||
Error bool `json:"error"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
HttpStatus int `json:"status"`
|
HttpStatus int `json:"status"`
|
||||||
|
Body []byte `json:"-"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
Ip string `json:"-"`
|
Ip string `json:"-"`
|
||||||
Duration time.Duration `json:"duration"`
|
Duration time.Duration `json:"duration"`
|
||||||
@ -59,7 +58,8 @@ func (service *Service) getIp(result *Result) {
|
|||||||
result.Ip = ips[0].String()
|
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{
|
client := &http.Client{
|
||||||
Timeout: time.Second * 10,
|
Timeout: time.Second * 10,
|
||||||
}
|
}
|
||||||
@ -71,13 +71,17 @@ func (service *Service) getStatus(result *Result) {
|
|||||||
}
|
}
|
||||||
result.Duration = time.Now().Sub(startTime)
|
result.Duration = time.Now().Sub(startTime)
|
||||||
result.HttpStatus = response.StatusCode
|
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 {
|
func (service *Service) EvaluateConditions() *Result {
|
||||||
result := &Result{Success: true, Errors: []string{}}
|
result := &Result{Success: true, Errors: []string{}}
|
||||||
service.getIp(result)
|
service.getIp(result)
|
||||||
if len(result.Errors) == 0 {
|
if len(result.Errors) == 0 {
|
||||||
service.getStatus(result)
|
service.call(result)
|
||||||
} else {
|
} else {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
@ -138,7 +142,13 @@ func sanitizeAndResolve(list []string, result *Result) []string {
|
|||||||
element = result.Ip
|
element = result.Ip
|
||||||
case ResponseTimePlaceHolder:
|
case ResponseTimePlaceHolder:
|
||||||
element = strconv.Itoa(int(result.Duration.Milliseconds()))
|
element = strconv.Itoa(int(result.Duration.Milliseconds()))
|
||||||
|
case BodyPlaceHolder:
|
||||||
|
element = string(result.Body)
|
||||||
default:
|
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)
|
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) {
|
func TestIntegrationEvaluateConditions(t *testing.T) {
|
||||||
condition := Condition("[STATUS] == 200")
|
condition := Condition("[STATUS] == 200")
|
||||||
service := Service{
|
service := Service{
|
||||||
Name: "GitHub",
|
Name: "TwiNNatioN",
|
||||||
Url: "https://api.github.com/healthz",
|
Url: "https://twinnation.org/health",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []*Condition{&condition},
|
||||||
}
|
}
|
||||||
result := service.EvaluateConditions()
|
result := service.EvaluateConditions()
|
||||||
@ -105,8 +177,8 @@ func TestIntegrationEvaluateConditions(t *testing.T) {
|
|||||||
func TestIntegrationEvaluateConditionsWithFailure(t *testing.T) {
|
func TestIntegrationEvaluateConditionsWithFailure(t *testing.T) {
|
||||||
condition := Condition("[STATUS] == 500")
|
condition := Condition("[STATUS] == 500")
|
||||||
service := Service{
|
service := Service{
|
||||||
Name: "GitHub",
|
Name: "TwiNNatioN",
|
||||||
Url: "https://api.github.com/healthz",
|
Url: "https://twinnation.org/health",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []*Condition{&condition},
|
||||||
}
|
}
|
||||||
result := service.EvaluateConditions()
|
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