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:
		
							
								
								
									
										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: | ||||
|  | ||||
| | 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          | | ||||
| | 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 "" | ||||
| } | ||||
		Reference in New Issue
	
	Block a user