mirror of
https://github.com/TwiN/gatus.git
synced 2025-01-24 23:08:58 +01:00
Merge branch 'master' into add-dns-feature
# Conflicts: # core/condition.go
This commit is contained in:
commit
cef94299e5
@ -1,3 +1,5 @@
|
|||||||
example
|
example
|
||||||
|
Dockerfile
|
||||||
.github
|
.github
|
||||||
.idea
|
.idea
|
||||||
|
.git
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Build the go application into a binary
|
# Build the go application into a binary
|
||||||
FROM golang:alpine as builder
|
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
|
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 Tests inside docker image if you don't have a configured go environment
|
||||||
#RUN apk update && apk add --virtual build-dependencies build-base gcc
|
#RUN apk update && apk add --virtual build-dependencies build-base gcc
|
||||||
|
@ -159,6 +159,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].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"}` |
|
| `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"}` |
|
| `[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 | 49h, 50h, 123h | 1h, 24h, ... |
|
||||||
|
|
||||||
|
|
||||||
#### Placeholders
|
#### Placeholders
|
||||||
@ -171,7 +172,7 @@ Here are some examples of conditions you can use:
|
|||||||
| `[IP]` | Resolves into the IP of the target host | 192.168.0.232
|
| `[IP]` | Resolves into the IP of the target host | 192.168.0.232
|
||||||
| `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}`
|
| `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}`
|
||||||
| `[CONNECTED]` | Resolves into whether a connection could be established | `true`
|
| `[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
|
#### Functions
|
||||||
|
@ -2,6 +2,7 @@ package slack
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
@ -18,8 +19,7 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// 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 {
|
func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||||
var message string
|
var message, color, results string
|
||||||
var color string
|
|
||||||
if resolved {
|
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)
|
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"
|
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)
|
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"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
var results string
|
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
var prefix string
|
var prefix string
|
||||||
if conditionResult.Success {
|
if conditionResult.Success {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
package slack
|
package slack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/TwinProduction/gatus/core"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
@ -2,11 +2,13 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/TwinProduction/gatus/jsonpath"
|
|
||||||
"github.com/TwinProduction/gatus/pattern"
|
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/jsonpath"
|
||||||
|
"github.com/TwinProduction/gatus/pattern"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -15,30 +17,30 @@ const (
|
|||||||
// Values that could replace the placeholder: 200, 404, 500, ...
|
// Values that could replace the placeholder: 200, 404, 500, ...
|
||||||
StatusPlaceholder = "[STATUS]"
|
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, ...
|
// Values that could replace the placeholder: 127.0.0.1, 10.0.0.1, ...
|
||||||
IPPlaceHolder = "[IP]"
|
IPPlaceholder = "[IP]"
|
||||||
|
|
||||||
// DNSRCodePlaceHolder is a place holder for DNS_RCODE
|
// DNSRCodePlaceHolder is a place holder for DNS_RCODE
|
||||||
//
|
//
|
||||||
// Values that could be NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP and REFUSED
|
// Values that could be NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP and REFUSED
|
||||||
DNSRCodePlaceHolder = "[DNS_RCODE]"
|
DNSRCodePlaceHolder = "[DNS_RCODE]"
|
||||||
|
|
||||||
// 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, ...
|
// 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"}}, ...
|
// 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
|
// Values that could replace the placeholder: true, false
|
||||||
ConnectedPlaceHolder = "[CONNECTED]"
|
ConnectedPlaceholder = "[CONNECTED]"
|
||||||
|
|
||||||
// CertificateExpirationPlaceholder is a placeholder for the duration before certificate expiration, in milliseconds.
|
// CertificateExpirationPlaceholder is a placeholder for the duration before certificate expiration, in milliseconds.
|
||||||
//
|
//
|
||||||
@ -144,27 +146,27 @@ func sanitizeAndResolve(list []string, result *Result) []string {
|
|||||||
switch strings.ToUpper(element) {
|
switch strings.ToUpper(element) {
|
||||||
case StatusPlaceholder:
|
case StatusPlaceholder:
|
||||||
element = strconv.Itoa(result.HTTPStatus)
|
element = strconv.Itoa(result.HTTPStatus)
|
||||||
case IPPlaceHolder:
|
case IPPlaceholder:
|
||||||
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 = body
|
||||||
case DNSRCodePlaceHolder:
|
case DNSRCodePlaceHolder:
|
||||||
element = result.DNSRCode
|
element = result.DNSRCode
|
||||||
case BodyPlaceHolder:
|
case ConnectedPlaceholder:
|
||||||
element = body
|
|
||||||
case ConnectedPlaceHolder:
|
|
||||||
element = strconv.FormatBool(result.Connected)
|
element = strconv.FormatBool(result.Connected)
|
||||||
case CertificateExpirationPlaceholder:
|
case CertificateExpirationPlaceholder:
|
||||||
element = strconv.FormatInt(int64(result.CertificateExpiration.Milliseconds()), 10)
|
element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10)
|
||||||
default:
|
default:
|
||||||
// if contains the BodyPlaceHolder, then evaluate json path
|
// if contains the BodyPlaceholder, then evaluate json path
|
||||||
if strings.Contains(element, BodyPlaceHolder) {
|
if strings.Contains(element, BodyPlaceholder) {
|
||||||
wantLength := false
|
wantLength := false
|
||||||
if strings.HasPrefix(element, LengthFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) {
|
if strings.HasPrefix(element, LengthFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) {
|
||||||
wantLength = true
|
wantLength = true
|
||||||
element = strings.TrimSuffix(strings.TrimPrefix(element, LengthFunctionPrefix), FunctionSuffix)
|
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 != nil {
|
||||||
if err.Error() != "unexpected end of JSON input" {
|
if err.Error() != "unexpected end of JSON input" {
|
||||||
result.Errors = append(result.Errors, err.Error())
|
result.Errors = append(result.Errors, err.Error())
|
||||||
@ -192,7 +194,9 @@ func sanitizeAndResolveNumerical(list []string, result *Result) []int64 {
|
|||||||
var sanitizedNumbers []int64
|
var sanitizedNumbers []int64
|
||||||
sanitizedList := sanitizeAndResolve(list, result)
|
sanitizedList := sanitizeAndResolve(list, result)
|
||||||
for _, element := range sanitizedList {
|
for _, element := range sanitizedList {
|
||||||
if number, err := strconv.ParseInt(element, 10, 64); 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
|
// Default to 0 if the string couldn't be converted to an integer
|
||||||
sanitizedNumbers = append(sanitizedNumbers, 0)
|
sanitizedNumbers = append(sanitizedNumbers, 0)
|
||||||
} else {
|
} else {
|
||||||
|
@ -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) {
|
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")
|
condition := Condition("[RESPONSE_TIME] > 500")
|
||||||
result := &Result{Duration: time.Millisecond * 750}
|
result := &Result{Duration: time.Millisecond * 750}
|
||||||
condition.evaluate(result)
|
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) {
|
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}
|
||||||
@ -320,7 +349,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()
|
acceptable := (time.Hour * 24 * 28).Milliseconds()
|
||||||
condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10))
|
condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10))
|
||||||
result := &Result{CertificateExpiration: time.Hour * 24 * 60}
|
result := &Result{CertificateExpiration: time.Hour * 24 * 60}
|
||||||
@ -330,7 +359,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()
|
acceptable := (time.Hour * 24 * 28).Milliseconds()
|
||||||
condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10))
|
condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10))
|
||||||
result := &Result{CertificateExpiration: time.Hour * 24 * 14}
|
result := &Result{CertificateExpiration: time.Hour * 24 * 14}
|
||||||
@ -339,3 +368,21 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanFailure(t *testin
|
|||||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -184,7 +184,7 @@ func (service *Service) call(result *Result) {
|
|||||||
result.Errors = append(result.Errors, err.Error())
|
result.Errors = append(result.Errors, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if response.TLS != nil {
|
if response.TLS != nil && len(response.TLS.PeerCertificates) > 0 {
|
||||||
certificate := response.TLS.PeerCertificates[0]
|
certificate := response.TLS.PeerCertificates[0]
|
||||||
result.CertificateExpiration = certificate.NotAfter.Sub(time.Now())
|
result.CertificateExpiration = certificate.NotAfter.Sub(time.Now())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user