mirror of
https://github.com/TwiN/gatus.git
synced 2025-02-16 10:20:00 +01:00
Add support for headers, method, body and json path with arrays
This commit is contained in:
parent
88d0d8a724
commit
fe3e60dbd4
19
client/client.go
Normal file
19
client/client.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
client *http.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetHttpClient() *http.Client {
|
||||||
|
if client == nil {
|
||||||
|
client = &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
25
config.yaml
25
config.yaml
@ -1,14 +1,21 @@
|
|||||||
metrics: true
|
metrics: true
|
||||||
services:
|
services:
|
||||||
- name: Twinnation
|
# - name: twinnation
|
||||||
url: https://twinnation.org/health
|
# interval: 10s
|
||||||
|
# url: https://twinnation.org/health
|
||||||
|
# conditions:
|
||||||
|
# - "[STATUS] == 200"
|
||||||
|
# - "[RESPONSE_TIME] < 1000"
|
||||||
|
# - "[BODY].status == UP"
|
||||||
|
- name: twinnation-articles-api
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
url: https://twinnation.org/api/v1/articles
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
- "[RESPONSE_TIME] < 500"
|
- "[BODY].[0].id == 42"
|
||||||
- "[BODY].status == UP"
|
|
||||||
- name: Example
|
# - name: example
|
||||||
url: https://example.org/
|
# url: https://example.org/
|
||||||
interval: 30s
|
# interval: 30s
|
||||||
conditions:
|
# conditions:
|
||||||
- "[STATUS] == 200"
|
# - "[STATUS] == 200"
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -74,9 +73,7 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
|||||||
} else {
|
} else {
|
||||||
// Set the default values if they aren't set
|
// Set the default values if they aren't set
|
||||||
for _, service := range config.Services {
|
for _, service := range config.Services {
|
||||||
if service.Interval == 0 {
|
service.Validate()
|
||||||
service.Interval = 10 * time.Second
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
51
core/condition.go
Normal file
51
core/condition.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Condition string
|
||||||
|
|
||||||
|
func (c *Condition) evaluate(result *Result) bool {
|
||||||
|
condition := string(*c)
|
||||||
|
success := false
|
||||||
|
var resolvedCondition string
|
||||||
|
if strings.Contains(condition, "==") {
|
||||||
|
parts := sanitizeAndResolve(strings.Split(condition, "=="), result)
|
||||||
|
success = parts[0] == parts[1]
|
||||||
|
resolvedCondition = fmt.Sprintf("%v == %v", parts[0], parts[1])
|
||||||
|
} else if strings.Contains(condition, "!=") {
|
||||||
|
parts := sanitizeAndResolve(strings.Split(condition, "!="), result)
|
||||||
|
success = parts[0] != parts[1]
|
||||||
|
resolvedCondition = fmt.Sprintf("%v != %v", parts[0], parts[1])
|
||||||
|
} else if strings.Contains(condition, "<=") {
|
||||||
|
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result)
|
||||||
|
success = parts[0] <= parts[1]
|
||||||
|
resolvedCondition = fmt.Sprintf("%v <= %v", parts[0], parts[1])
|
||||||
|
} else if strings.Contains(condition, ">=") {
|
||||||
|
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">="), result)
|
||||||
|
success = parts[0] >= parts[1]
|
||||||
|
resolvedCondition = fmt.Sprintf("%v >= %v", parts[0], parts[1])
|
||||||
|
} else if strings.Contains(condition, ">") {
|
||||||
|
parts := sanitizeAndResolveNumerical(strings.Split(condition, ">"), result)
|
||||||
|
success = parts[0] > parts[1]
|
||||||
|
resolvedCondition = fmt.Sprintf("%v > %v", parts[0], parts[1])
|
||||||
|
} else if strings.Contains(condition, "<") {
|
||||||
|
parts := sanitizeAndResolveNumerical(strings.Split(condition, "<"), result)
|
||||||
|
success = parts[0] < parts[1]
|
||||||
|
resolvedCondition = fmt.Sprintf("%v < %v", parts[0], parts[1])
|
||||||
|
} else {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("invalid condition '%s' has been provided", condition))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
conditionToDisplay := condition
|
||||||
|
// If the condition isn't a success, return the resolved condition
|
||||||
|
if !success {
|
||||||
|
log.Printf("[Condition][evaluate] Condition '%s' did not succeed because '%s' is false", condition, resolvedCondition)
|
||||||
|
conditionToDisplay = resolvedCondition
|
||||||
|
}
|
||||||
|
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
|
||||||
|
return success
|
||||||
|
}
|
@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEvaluateWithIp(t *testing.T) {
|
func TestCondition_evaluateWithIp(t *testing.T) {
|
||||||
condition := Condition("[IP] == 127.0.0.1")
|
condition := Condition("[IP] == 127.0.0.1")
|
||||||
result := &Result{Ip: "127.0.0.1"}
|
result := &Result{Ip: "127.0.0.1"}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -14,7 +14,7 @@ func TestEvaluateWithIp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithStatus(t *testing.T) {
|
func TestCondition_evaluateWithStatus(t *testing.T) {
|
||||||
condition := Condition("[STATUS] == 201")
|
condition := Condition("[STATUS] == 201")
|
||||||
result := &Result{HttpStatus: 201}
|
result := &Result{HttpStatus: 201}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -23,7 +23,7 @@ func TestEvaluateWithStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithStatusFailure(t *testing.T) {
|
func TestCondition_evaluateWithStatusFailure(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)
|
||||||
@ -32,7 +32,7 @@ func TestEvaluateWithStatusFailure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithStatusUsingLessThan(t *testing.T) {
|
func TestCondition_evaluateWithStatusUsingLessThan(t *testing.T) {
|
||||||
condition := Condition("[STATUS] < 300")
|
condition := Condition("[STATUS] < 300")
|
||||||
result := &Result{HttpStatus: 201}
|
result := &Result{HttpStatus: 201}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -41,7 +41,7 @@ func TestEvaluateWithStatusUsingLessThan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithStatusFailureUsingLessThan(t *testing.T) {
|
func TestCondition_evaluateWithStatusFailureUsingLessThan(t *testing.T) {
|
||||||
condition := Condition("[STATUS] < 300")
|
condition := Condition("[STATUS] < 300")
|
||||||
result := &Result{HttpStatus: 404}
|
result := &Result{HttpStatus: 404}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -50,7 +50,7 @@ func TestEvaluateWithStatusFailureUsingLessThan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithResponseTimeUsingLessThan(t *testing.T) {
|
func TestCondition_evaluateWithResponseTimeUsingLessThan(t *testing.T) {
|
||||||
condition := Condition("[RESPONSE_TIME] < 500")
|
condition := Condition("[RESPONSE_TIME] < 500")
|
||||||
result := &Result{Duration: time.Millisecond * 50}
|
result := &Result{Duration: time.Millisecond * 50}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -59,7 +59,7 @@ func TestEvaluateWithResponseTimeUsingLessThan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
|
func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
|
||||||
condition := Condition("[RESPONSE_TIME] > 500")
|
condition := Condition("[RESPONSE_TIME] > 500")
|
||||||
result := &Result{Duration: time.Millisecond * 750}
|
result := &Result{Duration: time.Millisecond * 750}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -68,7 +68,7 @@ func TestEvaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.T) {
|
func TestCondition_evaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.T) {
|
||||||
condition := Condition("[RESPONSE_TIME] >= 500")
|
condition := Condition("[RESPONSE_TIME] >= 500")
|
||||||
result := &Result{Duration: time.Millisecond * 500}
|
result := &Result{Duration: time.Millisecond * 500}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -77,7 +77,7 @@ func TestEvaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithResponseTimeUsingLessThanOrEqualTo(t *testing.T) {
|
func TestCondition_evaluateWithResponseTimeUsingLessThanOrEqualTo(t *testing.T) {
|
||||||
condition := Condition("[RESPONSE_TIME] <= 500")
|
condition := Condition("[RESPONSE_TIME] <= 500")
|
||||||
result := &Result{Duration: time.Millisecond * 500}
|
result := &Result{Duration: time.Millisecond * 500}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -86,7 +86,7 @@ func TestEvaluateWithResponseTimeUsingLessThanOrEqualTo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithBody(t *testing.T) {
|
func TestCondition_evaluateWithBody(t *testing.T) {
|
||||||
condition := Condition("[BODY] == test")
|
condition := Condition("[BODY] == test")
|
||||||
result := &Result{Body: []byte("test")}
|
result := &Result{Body: []byte("test")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -95,7 +95,7 @@ func TestEvaluateWithBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithBodyJsonPath(t *testing.T) {
|
func TestCondition_evaluateWithBodyJsonPath(t *testing.T) {
|
||||||
condition := Condition("[BODY].status == UP")
|
condition := Condition("[BODY].status == UP")
|
||||||
result := &Result{Body: []byte("{\"status\":\"UP\"}")}
|
result := &Result{Body: []byte("{\"status\":\"UP\"}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -104,7 +104,7 @@ func TestEvaluateWithBodyJsonPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithBodyJsonPathComplex(t *testing.T) {
|
func TestCondition_evaluateWithBodyJsonPathComplex(t *testing.T) {
|
||||||
condition := Condition("[BODY].data.name == john")
|
condition := Condition("[BODY].data.name == john")
|
||||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1, \"name\": \"john\"}}")}
|
result := &Result{Body: []byte("{\"data\": {\"id\": 1, \"name\": \"john\"}}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -113,7 +113,7 @@ func TestEvaluateWithBodyJsonPathComplex(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithBodyJsonPathComplexInt(t *testing.T) {
|
func TestCondition_evaluateWithBodyJsonPathLongInt(t *testing.T) {
|
||||||
condition := Condition("[BODY].data.id == 1")
|
condition := Condition("[BODY].data.id == 1")
|
||||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -122,7 +122,16 @@ func TestEvaluateWithBodyJsonPathComplexInt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithBodyJsonPathComplexIntUsingGreaterThan(t *testing.T) {
|
func TestCondition_evaluateWithBodyJsonPathComplexInt(t *testing.T) {
|
||||||
|
condition := Condition("[BODY].data[1].id == 2")
|
||||||
|
result := &Result{Body: []byte("{\"data\": [{\"id\": 1}, {\"id\": 2}, {\"id\": 3}]}")}
|
||||||
|
condition.evaluate(result)
|
||||||
|
if !result.ConditionResults[0].Success {
|
||||||
|
t.Errorf("Condition '%s' should have been a success", condition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCondition_evaluateWithBodyJsonPathComplexIntUsingGreaterThan(t *testing.T) {
|
||||||
condition := Condition("[BODY].data.id > 0")
|
condition := Condition("[BODY].data.id > 0")
|
||||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -131,7 +140,7 @@ func TestEvaluateWithBodyJsonPathComplexIntUsingGreaterThan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithBodyJsonPathComplexIntFailureUsingGreaterThan(t *testing.T) {
|
func TestCondition_evaluateWithBodyJsonPathComplexIntFailureUsingGreaterThan(t *testing.T) {
|
||||||
condition := Condition("[BODY].data.id > 5")
|
condition := Condition("[BODY].data.id > 5")
|
||||||
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
result := &Result{Body: []byte("{\"data\": {\"id\": 1}}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -140,7 +149,7 @@ func TestEvaluateWithBodyJsonPathComplexIntFailureUsingGreaterThan(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithBodyJsonPathComplexIntUsingLessThan(t *testing.T) {
|
func TestCondition_evaluateWithBodyJsonPathComplexIntUsingLessThan(t *testing.T) {
|
||||||
condition := Condition("[BODY].data.id < 5")
|
condition := Condition("[BODY].data.id < 5")
|
||||||
result := &Result{Body: []byte("{\"data\": {\"id\": 2}}")}
|
result := &Result{Body: []byte("{\"data\": {\"id\": 2}}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -149,7 +158,7 @@ func TestEvaluateWithBodyJsonPathComplexIntUsingLessThan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluateWithBodyJsonPathComplexIntFailureUsingLessThan(t *testing.T) {
|
func TestCondition_evaluateWithBodyJsonPathComplexIntFailureUsingLessThan(t *testing.T) {
|
||||||
condition := Condition("[BODY].data.id < 5")
|
condition := Condition("[BODY].data.id < 5")
|
||||||
result := &Result{Body: []byte("{\"data\": {\"id\": 10}}")}
|
result := &Result{Body: []byte("{\"data\": {\"id\": 10}}")}
|
||||||
condition.evaluate(result)
|
condition.evaluate(result)
|
||||||
@ -157,35 +166,3 @@ func TestEvaluateWithBodyJsonPathComplexIntFailureUsingLessThan(t *testing.T) {
|
|||||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationEvaluateConditions(t *testing.T) {
|
|
||||||
condition := Condition("[STATUS] == 200")
|
|
||||||
service := Service{
|
|
||||||
Name: "TwiNNatioN",
|
|
||||||
Url: "https://twinnation.org/health",
|
|
||||||
Conditions: []*Condition{&condition},
|
|
||||||
}
|
|
||||||
result := service.EvaluateConditions()
|
|
||||||
if !result.ConditionResults[0].Success {
|
|
||||||
t.Errorf("Condition '%s' should have been a success", condition)
|
|
||||||
}
|
|
||||||
if !result.Success {
|
|
||||||
t.Error("Because all conditions passed, this should have been a success")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntegrationEvaluateConditionsWithFailure(t *testing.T) {
|
|
||||||
condition := Condition("[STATUS] == 500")
|
|
||||||
service := Service{
|
|
||||||
Name: "TwiNNatioN",
|
|
||||||
Url: "https://twinnation.org/health",
|
|
||||||
Conditions: []*Condition{&condition},
|
|
||||||
}
|
|
||||||
result := service.EvaluateConditions()
|
|
||||||
if result.ConditionResults[0].Success {
|
|
||||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
|
||||||
}
|
|
||||||
if result.Success {
|
|
||||||
t.Error("Because one of the conditions failed, success should have been false")
|
|
||||||
}
|
|
||||||
}
|
|
95
core/service.go
Normal file
95
core/service.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/TwinProduction/gatus/client"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Interval time.Duration `yaml:"interval,omitempty"`
|
||||||
|
Url string `yaml:"url"`
|
||||||
|
Method string `yaml:"method,omitempty"`
|
||||||
|
Body string `yaml:"body,omitempty"`
|
||||||
|
Headers map[string]string `yaml:"headers"`
|
||||||
|
Conditions []*Condition `yaml:"conditions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) Validate() {
|
||||||
|
// Set default values
|
||||||
|
if service.Interval == 0 {
|
||||||
|
service.Interval = 10 * time.Second
|
||||||
|
}
|
||||||
|
if len(service.Method) == 0 {
|
||||||
|
service.Method = http.MethodGet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that the request can be created
|
||||||
|
_, err := http.NewRequest(service.Method, service.Url, bytes.NewBuffer([]byte(service.Body)))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) EvaluateConditions() *Result {
|
||||||
|
result := &Result{Success: true, Errors: []string{}}
|
||||||
|
service.getIp(result)
|
||||||
|
if len(result.Errors) == 0 {
|
||||||
|
service.call(result)
|
||||||
|
} else {
|
||||||
|
result.Success = false
|
||||||
|
}
|
||||||
|
for _, condition := range service.Conditions {
|
||||||
|
success := condition.evaluate(result)
|
||||||
|
if !success {
|
||||||
|
result.Success = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.Timestamp = time.Now()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) getIp(result *Result) {
|
||||||
|
urlObject, err := url.Parse(service.Url)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.Hostname = urlObject.Hostname()
|
||||||
|
ips, err := net.LookupIP(urlObject.Hostname())
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.Ip = ips[0].String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) call(result *Result) {
|
||||||
|
request := service.buildRequest()
|
||||||
|
startTime := time.Now()
|
||||||
|
response, err := client.GetHttpClient().Do(request)
|
||||||
|
if err != nil {
|
||||||
|
result.Duration = time.Since(startTime)
|
||||||
|
result.Errors = append(result.Errors, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.Duration = time.Since(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) buildRequest() *http.Request {
|
||||||
|
request, _ := http.NewRequest(service.Method, service.Url, bytes.NewBuffer([]byte(service.Body)))
|
||||||
|
for k, v := range service.Headers {
|
||||||
|
request.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
37
core/service_test.go
Normal file
37
core/service_test.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntegrationEvaluateConditions(t *testing.T) {
|
||||||
|
condition := Condition("[STATUS] == 200")
|
||||||
|
service := Service{
|
||||||
|
Name: "TwiNNatioN",
|
||||||
|
Url: "https://twinnation.org/health",
|
||||||
|
Conditions: []*Condition{&condition},
|
||||||
|
}
|
||||||
|
result := service.EvaluateConditions()
|
||||||
|
if !result.ConditionResults[0].Success {
|
||||||
|
t.Errorf("Condition '%s' should have been a success", condition)
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Error("Because all conditions passed, this should have been a success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationEvaluateConditionsWithFailure(t *testing.T) {
|
||||||
|
condition := Condition("[STATUS] == 500")
|
||||||
|
service := Service{
|
||||||
|
Name: "TwiNNatioN",
|
||||||
|
Url: "https://twinnation.org/health",
|
||||||
|
Conditions: []*Condition{&condition},
|
||||||
|
}
|
||||||
|
result := service.EvaluateConditions()
|
||||||
|
if result.ConditionResults[0].Success {
|
||||||
|
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||||
|
}
|
||||||
|
if result.Success {
|
||||||
|
t.Error("Because one of the conditions failed, success should have been false")
|
||||||
|
}
|
||||||
|
}
|
147
core/types.go
147
core/types.go
@ -1,24 +1,9 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/TwinProduction/gatus/jsonpath"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
StatusPlaceholder = "[STATUS]"
|
|
||||||
IPPlaceHolder = "[IP]"
|
|
||||||
ResponseTimePlaceHolder = "[RESPONSE_TIME]"
|
|
||||||
BodyPlaceHolder = "[BODY]"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HealthStatus struct {
|
type HealthStatus struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
@ -36,135 +21,7 @@ type Result struct {
|
|||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Url string `yaml:"url"`
|
|
||||||
Interval time.Duration `yaml:"interval,omitempty"`
|
|
||||||
Conditions []*Condition `yaml:"conditions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) getIp(result *Result) {
|
|
||||||
urlObject, err := url.Parse(service.Url)
|
|
||||||
if err != nil {
|
|
||||||
result.Errors = append(result.Errors, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result.Hostname = urlObject.Hostname()
|
|
||||||
ips, err := net.LookupIP(urlObject.Hostname())
|
|
||||||
if err != nil {
|
|
||||||
result.Errors = append(result.Errors, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result.Ip = ips[0].String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) call(result *Result) {
|
|
||||||
// TODO: re-use the same client instead of creating multiple clients
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: time.Second * 10,
|
|
||||||
}
|
|
||||||
startTime := time.Now()
|
|
||||||
response, err := client.Get(service.Url)
|
|
||||||
if err != nil {
|
|
||||||
result.Errors = append(result.Errors, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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.call(result)
|
|
||||||
} else {
|
|
||||||
result.Success = false
|
|
||||||
}
|
|
||||||
for _, condition := range service.Conditions {
|
|
||||||
success := condition.evaluate(result)
|
|
||||||
if !success {
|
|
||||||
result.Success = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.Timestamp = time.Now()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConditionResult struct {
|
type ConditionResult struct {
|
||||||
Condition *Condition `json:"condition"`
|
Condition string `json:"condition"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
success = parts[0] == parts[1]
|
|
||||||
} else if strings.Contains(condition, "!=") {
|
|
||||||
parts := sanitizeAndResolve(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 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 {
|
|
||||||
var sanitizedList []string
|
|
||||||
for _, element := range list {
|
|
||||||
element = strings.TrimSpace(element)
|
|
||||||
switch strings.ToUpper(element) {
|
|
||||||
case StatusPlaceholder:
|
|
||||||
element = strconv.Itoa(result.HttpStatus)
|
|
||||||
case IPPlaceHolder:
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
61
core/util.go
Normal file
61
core/util.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/TwinProduction/gatus/jsonpath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusPlaceholder = "[STATUS]"
|
||||||
|
IPPlaceHolder = "[IP]"
|
||||||
|
ResponseTimePlaceHolder = "[RESPONSE_TIME]"
|
||||||
|
BodyPlaceHolder = "[BODY]"
|
||||||
|
|
||||||
|
InvalidConditionElementSuffix = "(INVALID)"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sanitizeAndResolve(list []string, result *Result) []string {
|
||||||
|
var sanitizedList []string
|
||||||
|
for _, element := range list {
|
||||||
|
element = strings.TrimSpace(element)
|
||||||
|
switch strings.ToUpper(element) {
|
||||||
|
case StatusPlaceholder:
|
||||||
|
element = strconv.Itoa(result.HttpStatus)
|
||||||
|
case IPPlaceHolder:
|
||||||
|
element = result.Ip
|
||||||
|
case ResponseTimePlaceHolder:
|
||||||
|
element = strconv.Itoa(int(result.Duration.Milliseconds()))
|
||||||
|
case BodyPlaceHolder:
|
||||||
|
element = string(result.Body)
|
||||||
|
default:
|
||||||
|
// if starts with BodyPlaceHolder, then evaluate json path
|
||||||
|
if strings.HasPrefix(element, BodyPlaceHolder) {
|
||||||
|
resolvedElement, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceHolder), "", 1), result.Body)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, err.Error())
|
||||||
|
element = fmt.Sprintf("%s %s", element, InvalidConditionElementSuffix)
|
||||||
|
} else {
|
||||||
|
element = resolvedElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
@ -3,28 +3,69 @@ package jsonpath
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Eval(path string, b []byte) string {
|
func Eval(path string, b []byte) (string, error) {
|
||||||
var object map[string]interface{}
|
var object interface{}
|
||||||
err := json.Unmarshal(b, &object)
|
err := json.Unmarshal(b, &object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
// Try to unmarshall it into an array instead
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
return walk(path, object)
|
return walk(path, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
func walk(path string, object map[string]interface{}) string {
|
func walk(path string, object interface{}) (string, error) {
|
||||||
keys := strings.Split(path, ".")
|
keys := strings.Split(path, ".")
|
||||||
targetKey := keys[0]
|
currentKey := 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 there's more than one key, then walk deeper
|
||||||
if len(keys) > 0 {
|
if len(keys) > 1 {
|
||||||
return walk(strings.Replace(path, fmt.Sprintf("%s.", targetKey), "", 1), object[targetKey].(map[string]interface{}))
|
switch value := extractValue(currentKey, object).(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
return walk(strings.Replace(path, fmt.Sprintf("%s.", currentKey), "", 1), value)
|
||||||
|
case interface{}:
|
||||||
|
return fmt.Sprintf("%v", value), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("couldn't walk through '%s' because type was '%T', but expected 'map[string]interface{}'", currentKey, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ""
|
// if there's only one key and the target key is that key, then return its value
|
||||||
|
return fmt.Sprintf("%v", extractValue(currentKey, object)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractValue(currentKey string, value interface{}) interface{} {
|
||||||
|
// Check if the current key ends with [#]
|
||||||
|
if strings.HasSuffix(currentKey, "]") && strings.Contains(currentKey, "[") {
|
||||||
|
tmp := strings.SplitN(currentKey, "[", 3)
|
||||||
|
arrayIndex, err := strconv.Atoi(strings.Replace(tmp[1], "]", "", 1))
|
||||||
|
if err != nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
currentKey := tmp[0]
|
||||||
|
// if currentKey contains only an index (i.e. [0] or 0)
|
||||||
|
if len(currentKey) == 0 {
|
||||||
|
array := value.([]interface{})
|
||||||
|
if len(array) > arrayIndex {
|
||||||
|
if len(tmp) > 2 {
|
||||||
|
// Nested array? Go deeper.
|
||||||
|
return extractValue(fmt.Sprintf("%s[%s", currentKey, tmp[2]), array[arrayIndex])
|
||||||
|
}
|
||||||
|
return array[arrayIndex]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// if currentKey contains both a key and an index (i.e. data[0])
|
||||||
|
array := value.(map[string]interface{})[currentKey].([]interface{})
|
||||||
|
if len(array) > arrayIndex {
|
||||||
|
if len(tmp) > 2 {
|
||||||
|
// Nested array? Go deeper.
|
||||||
|
return extractValue(fmt.Sprintf("[%s", tmp[2]), array[arrayIndex])
|
||||||
|
}
|
||||||
|
return array[arrayIndex]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return value.(map[string]interface{})[currentKey]
|
||||||
}
|
}
|
||||||
|
148
jsonpath/jsonpath_test.go
Normal file
148
jsonpath/jsonpath_test.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package jsonpath
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEval(t *testing.T) {
|
||||||
|
path := "simple"
|
||||||
|
data := `{"simple": "value"}`
|
||||||
|
|
||||||
|
expectedOutput := "value"
|
||||||
|
|
||||||
|
output, err := Eval(path, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Didn't expect any error, but got", err)
|
||||||
|
}
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("Expected output to be %v, but was %v", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalWithLongSimpleWalk(t *testing.T) {
|
||||||
|
path := "long.simple.walk"
|
||||||
|
data := `{"long": {"simple": {"walk": "value"}}}`
|
||||||
|
|
||||||
|
expectedOutput := "value"
|
||||||
|
|
||||||
|
output, err := Eval(path, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Didn't expect any error, but got", err)
|
||||||
|
}
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("Expected output to be %v, but was %v", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalWithArrayOfMaps(t *testing.T) {
|
||||||
|
path := "ids[1].id"
|
||||||
|
data := `{"ids": [{"id": 1}, {"id": 2}]}`
|
||||||
|
|
||||||
|
expectedOutput := "2"
|
||||||
|
|
||||||
|
output, err := Eval(path, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Didn't expect any error, but got", err)
|
||||||
|
}
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("Expected output to be %v, but was %v", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalWithArrayOfValues(t *testing.T) {
|
||||||
|
path := "ids[0]"
|
||||||
|
data := `{"ids": [1, 2]}`
|
||||||
|
|
||||||
|
expectedOutput := "1"
|
||||||
|
|
||||||
|
output, err := Eval(path, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Didn't expect any error, but got", err)
|
||||||
|
}
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("Expected output to be %v, but was %v", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalWithRootArrayOfValues(t *testing.T) {
|
||||||
|
path := "[1]"
|
||||||
|
data := `[1, 2]`
|
||||||
|
|
||||||
|
expectedOutput := "2"
|
||||||
|
|
||||||
|
output, err := Eval(path, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Didn't expect any error, but got", err)
|
||||||
|
}
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("Expected output to be %v, but was %v", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalWithRootArrayOfMaps(t *testing.T) {
|
||||||
|
path := "[0].id"
|
||||||
|
data := `[{"id": 1}, {"id": 2}]`
|
||||||
|
|
||||||
|
expectedOutput := "1"
|
||||||
|
|
||||||
|
output, err := Eval(path, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Didn't expect any error, but got", err)
|
||||||
|
}
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("Expected output to be %v, but was %v", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalWithRootArrayOfMapsUsingInvalidArrayIndex(t *testing.T) {
|
||||||
|
path := "[5].id"
|
||||||
|
data := `[{"id": 1}, {"id": 2}]`
|
||||||
|
|
||||||
|
_, err := Eval(path, []byte(data))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should've returned an error, but didn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalWithLongWalkAndArray(t *testing.T) {
|
||||||
|
path := "data.ids[0].id"
|
||||||
|
data := `{"data": {"ids": [{"id": 1}, {"id": 2}, {"id": 3}]}}`
|
||||||
|
|
||||||
|
expectedOutput := "1"
|
||||||
|
|
||||||
|
output, err := Eval(path, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Didn't expect any error, but got", err)
|
||||||
|
}
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("Expected output to be %v, but was %v", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalWithNestedArray(t *testing.T) {
|
||||||
|
path := "[3][2]"
|
||||||
|
data := `[[1, 2], [3, 4], [], [5, 6, 7]]`
|
||||||
|
|
||||||
|
expectedOutput := "7"
|
||||||
|
|
||||||
|
output, err := Eval(path, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Didn't expect any error, but got", err)
|
||||||
|
}
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("Expected output to be %v, but was %v", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalWithMapOfNestedArray(t *testing.T) {
|
||||||
|
path := "data[1][1]"
|
||||||
|
data := `{"data": [["a", "b", "c"], ["d", "e", "f"]]}`
|
||||||
|
|
||||||
|
expectedOutput := "e"
|
||||||
|
|
||||||
|
output, err := Eval(path, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Didn't expect any error, but got", err)
|
||||||
|
}
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("Expected output to be %v, but was %v", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
22
main.go
22
main.go
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/TwinProduction/gatus/config"
|
"github.com/TwinProduction/gatus/config"
|
||||||
"github.com/TwinProduction/gatus/core"
|
|
||||||
"github.com/TwinProduction/gatus/watchdog"
|
"github.com/TwinProduction/gatus/watchdog"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"log"
|
"log"
|
||||||
@ -13,7 +12,6 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := loadConfiguration()
|
cfg := loadConfiguration()
|
||||||
go watchdog.Monitor(cfg)
|
|
||||||
http.HandleFunc("/api/v1/results", serviceResultsHandler)
|
http.HandleFunc("/api/v1/results", serviceResultsHandler)
|
||||||
http.HandleFunc("/health", healthHandler)
|
http.HandleFunc("/health", healthHandler)
|
||||||
http.Handle("/", http.FileServer(http.Dir("./static")))
|
http.Handle("/", http.FileServer(http.Dir("./static")))
|
||||||
@ -21,6 +19,7 @@ func main() {
|
|||||||
http.Handle("/metrics", promhttp.Handler())
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
}
|
}
|
||||||
log.Println("[main][main] Listening on port 8080")
|
log.Println("[main][main] Listening on port 8080")
|
||||||
|
go watchdog.Monitor(cfg)
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,21 +39,20 @@ func loadConfiguration() *config.Config {
|
|||||||
|
|
||||||
func serviceResultsHandler(writer http.ResponseWriter, _ *http.Request) {
|
func serviceResultsHandler(writer http.ResponseWriter, _ *http.Request) {
|
||||||
serviceResults := watchdog.GetServiceResults()
|
serviceResults := watchdog.GetServiceResults()
|
||||||
|
data, err := json.Marshal(serviceResults)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[main][serviceResultsHandler] Unable to marshall object to JSON: %s", err.Error())
|
||||||
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = writer.Write([]byte("Unable to marshall object to JSON"))
|
||||||
|
return
|
||||||
|
}
|
||||||
writer.Header().Add("Content-type", "application/json")
|
writer.Header().Add("Content-type", "application/json")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
_, _ = writer.Write(structToJsonBytes(serviceResults))
|
_, _ = writer.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func healthHandler(writer http.ResponseWriter, _ *http.Request) {
|
func healthHandler(writer http.ResponseWriter, _ *http.Request) {
|
||||||
writer.Header().Add("Content-type", "application/json")
|
writer.Header().Add("Content-type", "application/json")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
_, _ = writer.Write(structToJsonBytes(&core.HealthStatus{Status: "UP"}))
|
_, _ = writer.Write([]byte("{\"status\":\"UP\"}"))
|
||||||
}
|
|
||||||
|
|
||||||
func structToJsonBytes(obj interface{}) []byte {
|
|
||||||
bytes, err := json.Marshal(obj)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[main][structToJsonBytes] Unable to marshall object to JSON: %s", err.Error())
|
|
||||||
}
|
|
||||||
return bytes
|
|
||||||
}
|
}
|
||||||
|
@ -49,13 +49,13 @@
|
|||||||
let conditions = "";
|
let conditions = "";
|
||||||
for (let conditionResultIndex in serviceResult['condition-results']) {
|
for (let conditionResultIndex in serviceResult['condition-results']) {
|
||||||
let conditionResult = serviceResult['condition-results'][conditionResultIndex];
|
let conditionResult = serviceResult['condition-results'][conditionResultIndex];
|
||||||
conditions += "\n- " + (conditionResult.success ? "✓" : "X") + " ~ " + conditionResult.condition;
|
conditions += "\n" + (conditionResult.success ? "✓" : "X") + " ~ " + htmlEntities(conditionResult.condition);
|
||||||
}
|
}
|
||||||
output = output.replace("__CONDITIONS__", "\n\nConditions:" + conditions);
|
output = output.replace("__CONDITIONS__", "\n\nConditions:" + conditions);
|
||||||
if (serviceResult['errors'].length > 0) {
|
if (serviceResult['errors'].length > 0) {
|
||||||
let errors = "";
|
let errors = "";
|
||||||
for (let errorIndex in serviceResult['errors']) {
|
for (let errorIndex in serviceResult['errors']) {
|
||||||
errors += "\n- " + serviceResult['errors'][errorIndex];
|
errors += "\n- " + htmlEntities(serviceResult['errors'][errorIndex]);
|
||||||
}
|
}
|
||||||
output = output.replace("__ERRORS__", "\n\nErrors: " + errors);
|
output = output.replace("__ERRORS__", "\n\nErrors: " + errors);
|
||||||
} else {
|
} else {
|
||||||
@ -116,6 +116,15 @@
|
|||||||
return YYYY+"-"+MM+"-"+DD+" "+hh+":"+mm+":"+ss;
|
return YYYY+"-"+MM+"-"+DD+" "+hh+":"+mm+":"+ss;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function htmlEntities(s) {
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
refreshResults();
|
refreshResults();
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
refreshResults();
|
refreshResults();
|
||||||
|
@ -22,7 +22,7 @@ func Monitor(cfg *config.Config) {
|
|||||||
for _, service := range cfg.Services {
|
for _, service := range cfg.Services {
|
||||||
go monitor(service)
|
go monitor(service)
|
||||||
// To prevent multiple requests from running at the same time
|
// To prevent multiple requests from running at the same time
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(1111 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user