From 121369d9c082a2756d845774ab557c623bf404a7 Mon Sep 17 00:00:00 2001 From: Elouan Martinet Date: Sun, 15 Nov 2020 18:26:35 +0100 Subject: [PATCH 01/12] Add basic duration comparison --- core/condition.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/condition.go b/core/condition.go index 964416fd..cf13e87b 100644 --- a/core/condition.go +++ b/core/condition.go @@ -7,6 +7,7 @@ import ( "log" "strconv" "strings" + "time" ) const ( @@ -185,7 +186,9 @@ func sanitizeAndResolveNumerical(list []string, result *Result) []int64 { var sanitizedNumbers []int64 sanitizedList := sanitizeAndResolve(list, result) for _, element := range sanitizedList { - if number, err := strconv.ParseInt(element, 10, 64); err != nil { + if duration, err := time.ParseDuration(element); err == nil { + sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds()) + } else if number, err := strconv.ParseInt(element, 10, 64); err != nil { // Default to 0 if the string couldn't be converted to an integer sanitizedNumbers = append(sanitizedNumbers, 0) } else { From 108a88ae577906d0af99b686e04235a1a067e0cd Mon Sep 17 00:00:00 2001 From: Anastas Dancha Date: Mon, 16 Nov 2020 17:18:06 +0300 Subject: [PATCH 02/12] improving build caching - adding `Dockerfile` and `.git/` to `.dockeringore` improves caching when building images when these paths have changes unrelated to application functionality Signed-off-by: Anastas Dancha --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index c1f70b21..c7c11b97 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ example +Dockerfile .github .idea +.git From 573b5f89e15a457fb91c1d6cf6903c155c8f4540 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Mon, 16 Nov 2020 10:10:02 -0500 Subject: [PATCH 03/12] Improve test coverage --- core/condition.go | 7 ++++--- core/condition_test.go | 22 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/core/condition.go b/core/condition.go index cf13e87b..36cc9bf3 100644 --- a/core/condition.go +++ b/core/condition.go @@ -2,12 +2,13 @@ package core import ( "fmt" - "github.com/TwinProduction/gatus/jsonpath" - "github.com/TwinProduction/gatus/pattern" "log" "strconv" "strings" "time" + + "github.com/TwinProduction/gatus/jsonpath" + "github.com/TwinProduction/gatus/pattern" ) const ( @@ -186,7 +187,7 @@ func sanitizeAndResolveNumerical(list []string, result *Result) []int64 { var sanitizedNumbers []int64 sanitizedList := sanitizeAndResolve(list, result) for _, element := range sanitizedList { - if duration, err := time.ParseDuration(element); err == nil { + if duration, err := time.ParseDuration(element); duration != 0 && err == nil { sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds()) } else if number, err := strconv.ParseInt(element, 10, 64); err != nil { // Default to 0 if the string couldn't be converted to an integer diff --git a/core/condition_test.go b/core/condition_test.go index d4e929df..90890e58 100644 --- a/core/condition_test.go +++ b/core/condition_test.go @@ -320,7 +320,7 @@ func TestCondition_evaluateWithUnsetCertificateExpiration(t *testing.T) { } } -func TestCondition_evaluateWithCertificateExpirationGreaterThan(t *testing.T) { +func TestCondition_evaluateWithCertificateExpirationGreaterThanNumerical(t *testing.T) { acceptable := (time.Hour * 24 * 28).Milliseconds() condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10)) result := &Result{CertificateExpiration: time.Hour * 24 * 60} @@ -330,7 +330,7 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThan(t *testing.T) { } } -func TestCondition_evaluateWithCertificateExpirationGreaterThanFailure(t *testing.T) { +func TestCondition_evaluateWithCertificateExpirationGreaterThanNumericalFailure(t *testing.T) { acceptable := (time.Hour * 24 * 28).Milliseconds() condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10)) result := &Result{CertificateExpiration: time.Hour * 24 * 14} @@ -339,3 +339,21 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanFailure(t *testin t.Errorf("Condition '%s' should have been a failure", condition) } } + +func TestCondition_evaluateWithCertificateExpirationGreaterThanDuration(t *testing.T) { + condition := Condition("[CERTIFICATE_EXPIRATION] > 12h") + result := &Result{CertificateExpiration: 24 * time.Hour} + condition.evaluate(result) + if !result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a success", condition) + } +} + +func TestCondition_evaluateWithCertificateExpirationGreaterThanDurationFailure(t *testing.T) { + condition := Condition("[CERTIFICATE_EXPIRATION] > 48h") + result := &Result{CertificateExpiration: 24 * time.Hour} + condition.evaluate(result) + if result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a failure", condition) + } +} From 5699a1c236f87b39d1fdad7a6b00cda0a2592418 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Mon, 16 Nov 2020 10:27:46 -0500 Subject: [PATCH 04/12] Improve test coverage for duration parsing --- core/condition_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/condition_test.go b/core/condition_test.go index 90890e58..3aa3fc6d 100644 --- a/core/condition_test.go +++ b/core/condition_test.go @@ -60,7 +60,27 @@ func TestCondition_evaluateWithResponseTimeUsingLessThan(t *testing.T) { } } +func TestCondition_evaluateWithResponseTimeUsingLessThanDuration(t *testing.T) { + condition := Condition("[RESPONSE_TIME] < 1s") + 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 TestCondition_evaluateWithResponseTimeUsingLessThanInvalid(t *testing.T) { + condition := Condition("[RESPONSE_TIME] < potato") + result := &Result{Duration: time.Millisecond * 50} + condition.evaluate(result) + if result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have failed because the condition has an invalid numerical value that should've automatically resolved to 0", condition) + } +} + func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) { + // Not exactly sure why you'd want to have a condition that checks if the response time is too fast, + // but hey, who am I to judge? condition := Condition("[RESPONSE_TIME] > 500") result := &Result{Duration: time.Millisecond * 750} condition.evaluate(result) @@ -69,6 +89,15 @@ func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) { } } +func TestCondition_evaluateWithResponseTimeUsingGreaterThanDuration(t *testing.T) { + condition := Condition("[RESPONSE_TIME] > 1s") + result := &Result{Duration: time.Second * 2} + condition.evaluate(result) + if !result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a success", condition) + } +} + func TestCondition_evaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.T) { condition := Condition("[RESPONSE_TIME] >= 500") result := &Result{Duration: time.Millisecond * 500} From c23e21cb413ed9719bd69b69c24cc1964886af8b Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Mon, 16 Nov 2020 12:03:53 -0500 Subject: [PATCH 05/12] Update documentation for CERTIFICATE_EXPIRATION placeholder --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 063a852d..891e8ba2 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ Here are some examples of conditions you can use: | `len([BODY].data) < 5` | Array at JSONPath `$.data` has less than 5 elements | `{"data":[{"id":1}]}` | | | `len([BODY].name) == 8` | String at JSONPath `$.name` has a length of 8 | `{"name":"john.doe"}` | `{"name":"bob"}` | | `[BODY].name == pat(john*)` | String at JSONPath `$.name` matches pattern `john*` | `{"name":"john.doe"}` | `{"name":"bob"}` | +| `[CERTIFICATE_EXPIRATION] > 48h` | Certificate expiration is more than 48h away | `{"name":"john.doe"}` | `{"name":"bob"}` | #### Placeholders @@ -168,7 +169,7 @@ Here are some examples of conditions you can use: | `[IP]` | Resolves into the IP of the target host | 192.168.0.232 | `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}` | `[CONNECTED]` | Resolves into whether a connection could be established | `true` -| `[CERTIFICATE_EXPIRATION]` | Resolves into the duration before certificate expiration, in ms | 4461677039, 0 (if not using HTTPS) +| `[CERTIFICATE_EXPIRATION]` | Resolves into the duration before certificate expiration | `24h`, `48h`, 0 (if not using HTTPS) #### Functions From 2150942876eac1a626331ad2382aa1f4e1e4982a Mon Sep 17 00:00:00 2001 From: Elouan Martinet Date: Mon, 16 Nov 2020 18:16:11 +0100 Subject: [PATCH 06/12] Add day format support for duration comparison --- core/condition.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/condition.go b/core/condition.go index 36cc9bf3..1d48d7b9 100644 --- a/core/condition.go +++ b/core/condition.go @@ -3,6 +3,7 @@ package core import ( "fmt" "log" + "regexp" "strconv" "strings" "time" @@ -186,7 +187,15 @@ func sanitizeAndResolve(list []string, result *Result) []string { func sanitizeAndResolveNumerical(list []string, result *Result) []int64 { var sanitizedNumbers []int64 sanitizedList := sanitizeAndResolve(list, result) + dayRegex := regexp.MustCompile(`^\s*(\d+)d\s*$`) for _, element := range sanitizedList { + if matches := dayRegex.FindStringSubmatch(element); matches != nil { + if days, err := strconv.ParseInt(matches[1], 10, 64); err == nil { + duration := time.Duration(days * int64(time.Hour * 24)) + sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds()) + continue + } + } if duration, err := time.ParseDuration(element); duration != 0 && err == nil { sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds()) } else if number, err := strconv.ParseInt(element, 10, 64); err != nil { From 4a84368d2417db0ee598e756e1b1526bd4f3fba5 Mon Sep 17 00:00:00 2001 From: Anastas Dancha Date: Mon, 16 Nov 2020 17:19:12 +0300 Subject: [PATCH 07/12] use COPY instead of ADD, install pkgs then build - using COPY instead of ADD, since working with local paths, and not extracting archived data - installing packages before building application binary improves caching, and avoids installing packages every time the application code changes Signed-off-by: Anastas Dancha --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index d926f7c7..83bb78a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # Build the go application into a binary FROM golang:alpine as builder -WORKDIR /app -ADD . ./ -RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus . RUN apk --update add ca-certificates +WORKDIR /app +COPY . ./ +RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus . # Run Tests inside docker image if you don't have a configured go environment #RUN apk update && apk add --virtual build-dependencies build-base gcc @@ -17,4 +17,4 @@ COPY --from=builder /app/static static/ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt ENV PORT=8080 EXPOSE ${PORT} -ENTRYPOINT ["/gatus"] \ No newline at end of file +ENTRYPOINT ["/gatus"] From e79c849e6d5433a070f51d24c91bb45abbd857f7 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Tue, 17 Nov 2020 12:16:40 -0500 Subject: [PATCH 08/12] Revert "Add day format support for duration comparison" This reverts commit 21509428 --- core/condition.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/core/condition.go b/core/condition.go index 1d48d7b9..36cc9bf3 100644 --- a/core/condition.go +++ b/core/condition.go @@ -3,7 +3,6 @@ package core import ( "fmt" "log" - "regexp" "strconv" "strings" "time" @@ -187,15 +186,7 @@ func sanitizeAndResolve(list []string, result *Result) []string { func sanitizeAndResolveNumerical(list []string, result *Result) []int64 { var sanitizedNumbers []int64 sanitizedList := sanitizeAndResolve(list, result) - dayRegex := regexp.MustCompile(`^\s*(\d+)d\s*$`) for _, element := range sanitizedList { - if matches := dayRegex.FindStringSubmatch(element); matches != nil { - if days, err := strconv.ParseInt(matches[1], 10, 64); err == nil { - duration := time.Duration(days * int64(time.Hour * 24)) - sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds()) - continue - } - } if duration, err := time.ParseDuration(element); duration != 0 && err == nil { sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds()) } else if number, err := strconv.ParseInt(element, 10, 64); err != nil { From 5433653cbb4c5185edc73258c3d7f1920d1e3e34 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Tue, 17 Nov 2020 12:33:00 -0500 Subject: [PATCH 09/12] Fix bad example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 891e8ba2..81c65eed 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Here are some examples of conditions you can use: | `len([BODY].data) < 5` | Array at JSONPath `$.data` has less than 5 elements | `{"data":[{"id":1}]}` | | | `len([BODY].name) == 8` | String at JSONPath `$.name` has a length of 8 | `{"name":"john.doe"}` | `{"name":"bob"}` | | `[BODY].name == pat(john*)` | String at JSONPath `$.name` matches pattern `john*` | `{"name":"john.doe"}` | `{"name":"bob"}` | -| `[CERTIFICATE_EXPIRATION] > 48h` | Certificate expiration is more than 48h away | `{"name":"john.doe"}` | `{"name":"bob"}` | +| `[CERTIFICATE_EXPIRATION] > 48h` | Certificate expiration is more than 48h away | 49h, 50h, 123h | 1h, 24h, ... | #### Placeholders From be72a73082e1e04f4552440eac0caef08bb38970 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Tue, 17 Nov 2020 12:35:21 -0500 Subject: [PATCH 10/12] Fix potential panic --- core/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/service.go b/core/service.go index 0a025d8a..b3caa089 100644 --- a/core/service.go +++ b/core/service.go @@ -169,7 +169,7 @@ func (service *Service) call(result *Result) { result.Errors = append(result.Errors, err.Error()) return } - if response.TLS != nil { + if response.TLS != nil && len(response.TLS.PeerCertificates) > 0 { certificate := response.TLS.PeerCertificates[0] result.CertificateExpiration = certificate.NotAfter.Sub(time.Now()) } From 7e35a6ebbdcbbb7cd6a4d1b6da8f1b667550bad3 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Tue, 17 Nov 2020 19:34:22 -0500 Subject: [PATCH 11/12] Rename PlaceHolder to Placeholder --- core/condition.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/core/condition.go b/core/condition.go index 36cc9bf3..1dac1410 100644 --- a/core/condition.go +++ b/core/condition.go @@ -17,25 +17,25 @@ const ( // Values that could replace the placeholder: 200, 404, 500, ... StatusPlaceholder = "[STATUS]" - // IPPlaceHolder is a placeholder for an IP. + // IPPlaceholder is a placeholder for an IP. // // Values that could replace the placeholder: 127.0.0.1, 10.0.0.1, ... - IPPlaceHolder = "[IP]" + IPPlaceholder = "[IP]" - // ResponseTimePlaceHolder is a placeholder for the request response time, in milliseconds. + // ResponseTimePlaceholder is a placeholder for the request response time, in milliseconds. // // Values that could replace the placeholder: 1, 500, 1000, ... - ResponseTimePlaceHolder = "[RESPONSE_TIME]" + ResponseTimePlaceholder = "[RESPONSE_TIME]" - // BodyPlaceHolder is a placeholder for the body of the response + // BodyPlaceholder is a placeholder for the body of the response // // Values that could replace the placeholder: {}, {"data":{"name":"john"}}, ... - BodyPlaceHolder = "[BODY]" + BodyPlaceholder = "[BODY]" - // ConnectedPlaceHolder is a placeholder for whether a connection was successfully established. + // ConnectedPlaceholder is a placeholder for whether a connection was successfully established. // // Values that could replace the placeholder: true, false - ConnectedPlaceHolder = "[CONNECTED]" + ConnectedPlaceholder = "[CONNECTED]" // CertificateExpirationPlaceholder is a placeholder for the duration before certificate expiration, in milliseconds. // @@ -141,25 +141,25 @@ func sanitizeAndResolve(list []string, result *Result) []string { switch strings.ToUpper(element) { case StatusPlaceholder: element = strconv.Itoa(result.HTTPStatus) - case IPPlaceHolder: + case IPPlaceholder: element = result.IP - case ResponseTimePlaceHolder: + case ResponseTimePlaceholder: element = strconv.Itoa(int(result.Duration.Milliseconds())) - case BodyPlaceHolder: + case BodyPlaceholder: element = body - case ConnectedPlaceHolder: + case ConnectedPlaceholder: element = strconv.FormatBool(result.Connected) case CertificateExpirationPlaceholder: - element = strconv.FormatInt(int64(result.CertificateExpiration.Milliseconds()), 10) + element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10) default: - // if contains the BodyPlaceHolder, then evaluate json path - if strings.Contains(element, BodyPlaceHolder) { + // if contains the BodyPlaceholder, then evaluate json path + if strings.Contains(element, BodyPlaceholder) { wantLength := false if strings.HasPrefix(element, LengthFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) { wantLength = true element = strings.TrimSuffix(strings.TrimPrefix(element, LengthFunctionPrefix), FunctionSuffix) } - resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceHolder), "", 1), result.Body) + resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceholder), "", 1), result.Body) if err != nil { if err.Error() != "unexpected end of JSON input" { result.Errors = append(result.Errors, err.Error()) From 85c7308448f5925932aaaca5a33a0274d2926bc8 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Tue, 17 Nov 2020 19:34:35 -0500 Subject: [PATCH 12/12] Minor update --- alerting/provider/slack/slack.go | 5 ++--- alerting/provider/slack/slack_test.go | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/alerting/provider/slack/slack.go b/alerting/provider/slack/slack.go index a7d1f153..2c613a58 100644 --- a/alerting/provider/slack/slack.go +++ b/alerting/provider/slack/slack.go @@ -2,6 +2,7 @@ package slack import ( "fmt" + "github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/core" ) @@ -18,8 +19,7 @@ func (provider *AlertProvider) IsValid() bool { // ToCustomAlertProvider converts the provider into a custom.AlertProvider func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, result *core.Result, resolved bool) *custom.AlertProvider { - var message string - var color string + var message, color, results string if resolved { message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", service.Name, alert.SuccessThreshold) color = "#36A64F" @@ -27,7 +27,6 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", service.Name, alert.FailureThreshold) color = "#DD0000" } - var results string for _, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { diff --git a/alerting/provider/slack/slack_test.go b/alerting/provider/slack/slack_test.go index 9b853d5b..4f1c8d2d 100644 --- a/alerting/provider/slack/slack_test.go +++ b/alerting/provider/slack/slack_test.go @@ -1,9 +1,10 @@ package slack import ( - "github.com/TwinProduction/gatus/core" "strings" "testing" + + "github.com/TwinProduction/gatus/core" ) func TestAlertProvider_IsValid(t *testing.T) {